diff --git a/server/Cargo.lock b/server/Cargo.lock index de25e30..17998e8 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -62,47 +62,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -110,9 +111,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arc-swap" @@ -120,6 +121,21 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "area_module" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "tracing", + "utoipa", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -128,7 +144,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -140,11 +156,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -272,9 +294,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -326,9 +348,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -379,7 +401,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -390,9 +412,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "const-oid" @@ -466,9 +488,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "der" @@ -510,9 +532,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" dependencies = [ "serde", ] @@ -520,32 +542,83 @@ dependencies = [ [[package]] name = "elerp" version = "0.1.0" +dependencies = [ + "area_module", + "elerp_common", + "elerp_service", + "guest_order_module", + "inventory_module", + "order_category_module", + "order_module", + "order_payment_module", + "person_module", + "public_system", + "sku_category_module", + "sku_module", + "statistical_module", + "tokio", + "tracing", + "tracing-subscriber", + "user_system", + "warehouse_module", +] + +[[package]] +name = "elerp_common" +version = "0.1.0" dependencies = [ "ahash", "anyhow", + "clap", + "futures", + "futures-util", + "regex", + "serde", + "serde_json", + "sqlx", + "strum", + "tempfile", + "tokio", + "toml", + "tracing", + "utoipa", +] + +[[package]] +name = "elerp_service" +version = "0.1.0" +dependencies = [ + "anyhow", + "area_module", "axum", "axum-extra", "axum-server", - "chrono", - "clap", + "elerp_common", "futures", - "rand", + "guest_order_module", + "inventory_module", + "order_category_module", + "order_module", + "order_payment_module", + "person_module", + "public_system", "rcgen", - "regex", - "rust_xlsxwriter", "serde", "serde_json", "serde_qs", + "sku_category_module", + "sku_module", "sqlx", + "statistical_module", "strum", "tokio", "tokio-util", - "toml", "tower-http", "tracing", - "tracing-subscriber", + "user_system", "utoipa", "utoipa-rapidoc", + "warehouse_module", ] [[package]] @@ -556,9 +629,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -583,9 +656,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "finl_unicode" @@ -595,9 +668,9 @@ checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -696,7 +769,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -741,9 +814,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -756,17 +829,34 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "guest_order_module" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", + "user_system", + "utoipa", +] + [[package]] name = "h2" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http", "indexmap", "slab", @@ -777,9 +867,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -908,9 +998,9 @@ dependencies = [ [[package]] name = "http-range-header" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" +checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" [[package]] name = "httparse" @@ -1004,6 +1094,31 @@ dependencies = [ "serde", ] +[[package]] +name = "inventory_module" +version = "0.1.0" +dependencies = [ + "ahash", + "anyhow", + "elerp_common", + "futures", + "order_module", + "public_system", + "rust_xlsxwriter", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", + "utoipa", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.12.1" @@ -1039,9 +1154,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -1062,15 +1177,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1128,9 +1243,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -1200,9 +1315,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -1211,9 +1326,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -1244,6 +1359,55 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "order_category_module" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "tracing", + "utoipa", +] + +[[package]] +name = "order_module" +version = "0.1.0" +dependencies = [ + "ahash", + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "strum", + "tokio", + "tracing", + "user_system", + "utoipa", +] + +[[package]] +name = "order_payment_module" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "tracing", + "utoipa", +] + [[package]] name = "overload" version = "0.1.1" @@ -1252,9 +1416,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -1262,22 +1426,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem" @@ -1285,7 +1449,7 @@ version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "serde", ] @@ -1304,6 +1468,22 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "person_module" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", + "utoipa", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -1321,7 +1501,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -1401,13 +1581,32 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] +[[package]] +name = "public_system" +version = "0.1.0" +dependencies = [ + "ahash", + "anyhow", + "chrono", + "elerp_common", + "futures", + "rand", + "serde", + "serde_json", + "sqlx", + "strum", + "tokio", + "tracing", + "utoipa", +] + [[package]] name = "quote" version = "1.0.36" @@ -1468,6 +1667,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "regex" version = "1.10.4" @@ -1560,9 +1768,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" @@ -1579,9 +1787,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.11" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring 0.17.8", @@ -1604,15 +1812,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -1626,15 +1834,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scopeguard" @@ -1654,29 +1862,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.198" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -1707,9 +1915,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -1776,6 +1984,36 @@ dependencies = [ "rand_core", ] +[[package]] +name = "sku_category_module" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "tracing", + "utoipa", +] + +[[package]] +name = "sku_module" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "tracing", + "utoipa", +] + [[package]] name = "slab" version = "0.4.9" @@ -1793,9 +2031,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2033,6 +2271,24 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "statistical_module" +version = "0.1.0" +dependencies = [ + "ahash", + "anyhow", + "elerp_common", + "futures", + "order_module", + "public_system", + "serde", + "serde_json", + "sqlx", + "tokio", + "tracing", + "utoipa", +] + [[package]] name = "stringprep" version = "0.1.4" @@ -2069,7 +2325,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -2091,9 +2347,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" dependencies = [ "proc-macro2", "quote", @@ -2126,22 +2382,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -2215,7 +2471,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -2253,23 +2509,22 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] name = "toml" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba" dependencies = [ "serde", "serde_spanned", @@ -2279,18 +2534,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap", "serde", @@ -2372,7 +2627,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -2506,6 +2761,23 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "user_system" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures", + "public_system", + "serde", + "serde_json", + "sqlx", + "strum", + "tokio", + "tracing", + "utoipa", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -2520,9 +2792,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utoipa" -version = "4.2.0" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" +checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" dependencies = [ "indexmap", "serde", @@ -2532,15 +2804,15 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3c9f4d08338c1bfa70dde39412a040a884c6f318b3d09aaaf3437a1e52027fc" +checksum = "7bf0e16c02bc4bf5322ab65f10ab1149bdbcaa782cba66dc7057370a3f8190be" dependencies = [ "proc-macro-error", "proc-macro2", "quote", "regex", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -2573,6 +2845,22 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "warehouse_module" +version = "0.1.0" +dependencies = [ + "anyhow", + "elerp_common", + "futures-util", + "public_system", + "serde", + "serde_json", + "sqlx", + "tracing", + "user_system", + "utoipa", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2606,7 +2894,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", "wasm-bindgen-shared", ] @@ -2628,7 +2916,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2661,7 +2949,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" dependencies = [ - "redox_syscall", + "redox_syscall 0.4.1", "wasite", ] @@ -2837,9 +3125,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] @@ -2855,22 +3143,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index 90b3df4..7d1133e 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -4,12 +4,30 @@ version = "0.1.0" edition = "2021" [profile.release] -strip = true # Automatically strip symbols from the binary. +strip = true # Automatically strip symbols from the binary. lto = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] +[workspace] +members = ["crates/*"] + +[workspace.dependencies] +public_system = { version = "*", path = "crates/public_system" } +user_system = { version = "*", path = "crates/user_system" } +elerp_service = { version = "*", path = "crates/elerp_service" } +elerp_common = { version = "*", path = "crates/elerp_common" } +area_module = { version = "*", path = "crates/area_module" } +person_module = { version = "*", path = "crates/person_module" } +warehouse_module = { version = "*", path = "crates/warehouse_module" } +sku_module = { version = "*", path = "crates/sku_module" } +sku_category_module = { version = "*", path = "crates/sku_category_module" } +order_module = { version = "*", path = "crates/order_module" } +order_category_module = { version = "*", path = "crates/order_category_module" } +order_payment_module = { version = "*", path = "crates/order_payment_module" } +guest_order_module = { version = "*", path = "crates/guest_order_module" } +statistical_module = { version = "*", path = "crates/statistical_module" } +inventory_module = { version = "*", path = "crates/inventory_module" } serde = "1" serde_json = "1.0" @@ -32,7 +50,8 @@ sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "sqlite"] } anyhow = "1.0" clap = { version = "4.4.7", features = ["derive"] } -futures = "0.3.29" +futures = "0.3.30" +futures-util = "0.3.30" utoipa = { version = "4", features = ["axum_extras"] } utoipa-rapidoc = { version = "2", features = ["axum"] } @@ -42,7 +61,34 @@ strum = { version = "0.25", features = ["derive"] } rcgen = "0.11.3" rust_xlsxwriter = "0.58.0" tokio-util = "0.7.10" -chrono = { version = "0.4", default-features = false, features = ["std", "clock"] } +chrono = { version = "0.4", default-features = false, features = [ + "std", + "clock", +] } ahash = "0.8.8" regex = "1.10.3" toml = "0.8" + +[dependencies] +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +elerp_common = { workspace = true } +elerp_service = { workspace = true } + +[dev-dependencies] +public_system = { version = "*", workspace = true } +user_system = { version = "*", workspace = true } +elerp_service = { version = "*", workspace = true } +elerp_common = { version = "*", workspace = true } +area_module = { version = "*", workspace = true } +person_module = { version = "*", workspace = true } +warehouse_module = { version = "*", workspace = true } +sku_module = { version = "*", workspace = true } +sku_category_module = { version = "*", workspace = true } +order_module = { version = "*", workspace = true } +order_category_module = { version = "*", workspace = true } +order_payment_module = { version = "*", workspace = true } +guest_order_module = { version = "*", workspace = true } +statistical_module = { version = "*", workspace = true } +inventory_module = { version = "*", workspace = true } diff --git a/server/crates/area_module/Cargo.toml b/server/crates/area_module/Cargo.toml new file mode 100644 index 0000000..6c22ace --- /dev/null +++ b/server/crates/area_module/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "area_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa = { workspace = true } +futures = { workspace = true } \ No newline at end of file diff --git a/server/src/erp/area_module.rs b/server/crates/area_module/src/lib.rs similarity index 59% rename from server/src/erp/area_module.rs rename to server/crates/area_module/src/lib.rs index cdd66d6..690a17f 100644 --- a/server/src/erp/area_module.rs +++ b/server/crates/area_module/src/lib.rs @@ -1,16 +1,18 @@ use anyhow::bail; -use futures::TryStreamExt; -use sqlx::{Row, SqliteConnection}; - -use self::model::{Area, GetAreasQuery}; -use crate::public_system::{ +use anyhow::Result; +use elerp_common::sql; +use elerp_common::sql::get_row_from_table; +use elerp_common::sql::is_exists_in_table; +use elerp_common::sql::remove_row_from_table; +use elerp_common::sql::row_is_duplicate_col_in_table; +use elerp_common::sql::rows_to_objects; +use elerp_common::{ + area_module::model::area::{Area, GetAreasQuery}, model::{Pagination, WebSocketFlags}, - PublicSystem, }; - -use super::Result; - -pub mod model; +use futures::TryStreamExt; +use public_system::PublicSystem; +use sqlx::{Row, SqliteConnection}; #[derive(Debug, Clone)] pub struct AreaModule { @@ -45,51 +47,36 @@ impl AreaModule { } pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { - let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM areas;") - .fetch_one(&mut *tx) - .await? - .get("count"); + let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM areas;").fetch_one(&mut *tx).await?.get("count"); Ok(count >= self.ps.get_config().limit.areas) } - pub async fn is_exists_name( - &self, - name: &str, - prev: Option, - tx: &mut SqliteConnection, - ) -> Result { - self.ps - .row_is_duplicate_col_in_table(name, prev, "areas", "name", tx) - .await + pub async fn is_exists_name(&self, name: &str, prev: Option, tx: &mut SqliteConnection) -> Result { + row_is_duplicate_col_in_table(name, prev, "areas", "name", tx).await } pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps.is_exists_in_table("areas", "id", id, tx).await + is_exists_in_table("areas", "id", id, tx).await } pub async fn add(&self, mut v: Area, tx: &mut SqliteConnection) -> Result { - let r = sqlx::query( - "INSERT INTO areas (name, description, color, text_color) VALUES(?, ?, ?, ?)", - ) - .bind(&v.name) - .bind(&v.description) - .bind(&v.color) - .bind(&v.text_color) - .execute(&mut *tx) - .await?; + let r = sqlx::query("INSERT INTO areas (name, description, color, text_color) VALUES(?, ?, ?, ?)") + .bind(&v.name) + .bind(&v.description) + .bind(&v.color) + .bind(&v.text_color) + .execute(&mut *tx) + .await?; if r.rows_affected() != 1 { bail!("Can't add area"); } - v.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "areas", tx) - .await?; + v.id = sql::try_set_standard_id(r.last_insert_rowid(), "areas", tx).await?; self.ps.notice(WebSocketFlags::AddArea(v.id)).await?; Ok(v) } pub async fn remove(&self, id: i64, notice: bool, tx: &mut SqliteConnection) -> Result { - let r = self.ps.remove_row_from_table(id, "areas", tx).await?; + let r = remove_row_from_table(id, "areas", tx).await?; if notice { self.ps.notice(WebSocketFlags::RemoveArea(id)).await?; } @@ -97,15 +84,10 @@ impl AreaModule { } pub async fn get(&self, id: i64, tx: &mut SqliteConnection) -> Result> { - self.ps.get_row_from_table("areas", "id", id, tx).await + get_row_from_table("areas", "id", id, tx).await } - pub async fn get_multiple( - &self, - pagination: &Pagination, - query: &GetAreasQuery, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple(&self, pagination: &Pagination, query: &GetAreasQuery, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let ob = query.get_order_condition(); let rows = sqlx::query(&format!( @@ -123,15 +105,10 @@ impl AreaModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects(rows) + rows_to_objects(rows) } - pub async fn get_multiple_ids( - &self, - pagination: &Pagination, - query: &GetAreasQuery, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple_ids(&self, pagination: &Pagination, query: &GetAreasQuery, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let rows = sqlx::query(&format!( "SELECT @@ -148,27 +125,19 @@ impl AreaModule { pub async fn get_count(&self, query: &GetAreasQuery, tx: &mut SqliteConnection) -> Result { let qw = query.get_where_condition(); - let row = sqlx::query(&format!("SELECT count(*) as count FROM areas {qw}")) - .fetch_one(&mut *tx) - .await?; + let row = sqlx::query(&format!("SELECT count(*) as count FROM areas {qw}")).fetch_one(&mut *tx).await?; Ok(row.get("count")) } - pub async fn update( - &self, - id: i64, - mut v: Area, - tx: &mut SqliteConnection, - ) -> Result> { - let r = - sqlx::query("UPDATE areas SET name=?, description=?, color=?, text_color=? WHERE id=?") - .bind(&v.name) - .bind(&v.description) - .bind(&v.color) - .bind(&v.text_color) - .bind(id) - .execute(&mut *tx) - .await?; + pub async fn update(&self, id: i64, mut v: Area, tx: &mut SqliteConnection) -> Result> { + let r = sqlx::query("UPDATE areas SET name=?, description=?, color=?, text_color=? WHERE id=?") + .bind(&v.name) + .bind(&v.description) + .bind(&v.color) + .bind(&v.text_color) + .bind(id) + .execute(&mut *tx) + .await?; Ok(if r.rows_affected() == 1 { v.id = id; self.ps.notice(WebSocketFlags::UpdateArea(id)).await?; @@ -179,17 +148,7 @@ impl AreaModule { } pub async fn is_depend_by_another(&self, id: i64, tx: &mut SqliteConnection) -> Result { - Ok(sqlx::query("SELECT id FROM persons WHERE area_id=?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some() - || sqlx::query("SELECT id FROM warehouses WHERE area_id=?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some()) + Ok(sqlx::query("SELECT id FROM persons WHERE area_id=?").bind(id).fetch(&mut *tx).try_next().await?.is_some() + || sqlx::query("SELECT id FROM warehouses WHERE area_id=?").bind(id).fetch(&mut *tx).try_next().await?.is_some()) } } diff --git a/server/crates/elerp_common/Cargo.toml b/server/crates/elerp_common/Cargo.toml new file mode 100644 index 0000000..9cec1c4 --- /dev/null +++ b/server/crates/elerp_common/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "elerp_common" +version = "0.1.0" +edition = "2021" + +[dependencies] +tempfile = "3" +sqlx = { workspace = true } +toml = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa = { workspace = true } +strum = { workspace = true } +ahash = { workspace = true } +tracing = { workspace = true } +tokio = { workspace = true } +clap = { workspace = true } +anyhow = { workspace = true } +regex = { workspace = true } +futures = { workspace = true } +futures-util = { workspace = true } diff --git a/server/crates/elerp_common/src/area_module.rs b/server/crates/elerp_common/src/area_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/area_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/crates/elerp_common/src/area_module/model.rs b/server/crates/elerp_common/src/area_module/model.rs new file mode 100644 index 0000000..31c7df9 --- /dev/null +++ b/server/crates/elerp_common/src/area_module/model.rs @@ -0,0 +1 @@ +pub mod area; diff --git a/server/src/erp/area_module/model/area.rs b/server/crates/elerp_common/src/area_module/model/area.rs similarity index 92% rename from server/src/erp/area_module/model/area.rs rename to server/crates/elerp_common/src/area_module/model/area.rs index afe37ef..e359649 100644 --- a/server/src/erp/area_module/model/area.rs +++ b/server/crates/elerp_common/src/area_module/model/area.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use sqlx::prelude::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::erp::util::{get_search_where_condition, get_sort_col_str, get_sorter_str}; +use crate::sql::{get_search_where_condition, get_sort_col_str, get_sorter_str}; #[derive(Debug, Serialize, Deserialize, ToSchema, FromRow)] pub struct Area { @@ -17,7 +17,7 @@ pub struct Area { pub text_color: Option, } -#[derive(Debug, Deserialize, ToSchema, IntoParams)] +#[derive(Debug, Default, Deserialize, ToSchema, IntoParams)] pub struct GetAreasQuery { pub id: Option, pub name: Option, diff --git a/server/src/config.rs b/server/crates/elerp_common/src/config.rs similarity index 99% rename from server/src/config.rs rename to server/crates/elerp_common/src/config.rs index 3d1086a..ed7008f 100644 --- a/server/src/config.rs +++ b/server/crates/elerp_common/src/config.rs @@ -1,10 +1,11 @@ use std::path::PathBuf; +use crate::i64_safe_max; use serde::Deserialize; use tokio::fs; use tracing::warn; -use crate::{meta::MetaInfo, myhelper::i64_safe_max}; +use crate::meta::MetaInfo; #[derive(Debug, Deserialize)] struct AppConfigInternal { diff --git a/server/crates/elerp_common/src/guest_order_module.rs b/server/crates/elerp_common/src/guest_order_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/guest_order_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/crates/elerp_common/src/guest_order_module/model.rs b/server/crates/elerp_common/src/guest_order_module/model.rs new file mode 100644 index 0000000..d7fda86 --- /dev/null +++ b/server/crates/elerp_common/src/guest_order_module/model.rs @@ -0,0 +1 @@ +pub mod guest_order; diff --git a/server/src/erp/guest_order_module/model/guest_order.rs b/server/crates/elerp_common/src/guest_order_module/model/guest_order.rs similarity index 93% rename from server/src/erp/guest_order_module/model/guest_order.rs rename to server/crates/elerp_common/src/guest_order_module/model/guest_order.rs index 3facf92..de6977d 100644 --- a/server/src/erp/guest_order_module/model/guest_order.rs +++ b/server/crates/elerp_common/src/guest_order_module/model/guest_order.rs @@ -1,18 +1,19 @@ +use crate::{ + set_to_string, + sql::{eq_or_not, get_sort_col_str, get_sorter_str, in_or_not, like_or_not}, +}; use ahash::HashSet; use serde::{Deserialize, Serialize}; use sqlx::prelude::Type; use strum::AsRefStr; use utoipa::{IntoParams, ToSchema}; -use crate::{ - erp::{ - order_module::model::{CheckOrderResult, Order, OrderCurrency, OrderItem, OrderType}, - util::{eq_or_not, get_sort_col_str, get_sorter_str, in_or_not, like_or_not}, - }, - myhelper::set_to_string, +use crate::order_module::model::{ + check_order_result::CheckOrderResult, + order::{Order, OrderCurrency, OrderItem, OrderPaymentStatus, OrderType}, }; -#[derive(Debug, Serialize, Deserialize, ToSchema, AsRefStr, Type)] +#[derive(Debug, Serialize, Deserialize, Clone, ToSchema, AsRefStr, Type, PartialEq, Eq, PartialOrd, Ord)] pub enum GuestOrderStatus { Confirmed, Pending, @@ -25,7 +26,7 @@ impl Default for GuestOrderStatus { } } -#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)] pub struct GuestOrder { #[serde(default)] pub id: i64, @@ -47,14 +48,14 @@ pub struct GuestOrder { pub person_related_id: i64, #[serde(default)] pub description: String, - + pub order_type: OrderType, - + #[serde(default)] pub guest_order_status: GuestOrderStatus, #[serde(default)] pub order_id: i64, - #[serde(default)] + pub order_category_id: i64, #[serde(default)] pub items: Option>, @@ -66,7 +67,7 @@ pub struct GuestOrderConfirm { pub order: Option, } -#[derive(Debug, Deserialize, ToSchema, IntoParams)] +#[derive(Debug, Deserialize, ToSchema, IntoParams, Default)] pub struct GetGuestOrdersQuery { pub id: Option, pub created_by_user_id: Option, @@ -104,7 +105,7 @@ impl From for Order { items: value.items, total_amount: 0.0, total_amount_settled: 0.0, - order_payment_status: crate::erp::order_module::model::OrderPaymentStatus::None, + order_payment_status: OrderPaymentStatus::None, warehouse_id: value.warehouse_id, person_related_id: value.person_related_id, description: value.description, diff --git a/server/crates/elerp_common/src/inventory_module.rs b/server/crates/elerp_common/src/inventory_module.rs new file mode 100644 index 0000000..3b9f9ac --- /dev/null +++ b/server/crates/elerp_common/src/inventory_module.rs @@ -0,0 +1,124 @@ +use ahash::{HashSet, HashSetExt}; +use anyhow::{bail, Result}; +use sqlx::{Row, SqliteConnection}; +use tracing::warn; + +use crate::order_module::model::order::{OrderItem, OrderType}; + +use self::model::{inventory::InventoryProduct, virtual_inventory::VirtualInventory}; + +pub mod model; + +pub fn calc_quantity_by_order_type(mut inventory_quantity: i64, item: &OrderItem, order_type: OrderType) -> i64 { + match order_type { + OrderType::Return | OrderType::StockIn => { + if !item.exchanged { + inventory_quantity += item.quantity + } + } + OrderType::StockOut => { + if !item.exchanged { + inventory_quantity -= item.quantity + } + } + OrderType::Exchange => { + if item.exchanged { + inventory_quantity += item.quantity + } else { + inventory_quantity -= item.quantity + } + } + OrderType::Calibration | OrderType::CalibrationStrict => { + if !item.exchanged { + inventory_quantity = item.quantity; + } + } + OrderType::Verification | OrderType::VerificationStrict => (), + }; + inventory_quantity +} + +pub fn get_virtual(capicity: usize) -> VirtualInventory { + VirtualInventory::new(capicity) +} + +async fn add(warehouse_id: i64, sku_id: i64, sku_category_id: i64, quantity: i64, tx: &mut SqliteConnection) -> Result> { + let r = sqlx::query("INSERT INTO inventory (warehouse_id, sku_id, sku_category_id, quantity) VALUES (?, ?, ?, ?)") + .bind(warehouse_id) + .bind(sku_id) + .bind(sku_category_id) + .bind(quantity) + .execute(&mut *tx) + .await?; + + Ok(if r.rows_affected() != 1 { + None + } else { + Some(InventoryProduct { + warehouse_id, + sku_id, + sku_category_id, + quantity, + }) + }) +} + +async fn update(product: InventoryProduct, tx: &mut SqliteConnection) -> Result> { + let r = sqlx::query("UPDATE inventory SET quantity=? WHERE warehouse_id = ? AND sku_id = ?") + .bind(product.quantity) + .bind(product.warehouse_id) + .bind(product.sku_id) + .execute(&mut *tx) + .await?; + + Ok(if r.rows_affected() != 1 { None } else { Some(product) }) +} + +pub async fn change(warehouse_id: i64, items: &Vec, order_type: OrderType, tx: &mut SqliteConnection) -> Result<()> { + let mut products = HashSet::with_capacity(items.len()); + let mut inventory = get_virtual(items.len()); + + if order_type == OrderType::CalibrationStrict { + sqlx::query("UPDATE inventory SET quantity=0 WHERE warehouse_id=?").bind(warehouse_id).execute(&mut *tx).await?; + } + for item in items { + if item.exchanged && order_type != OrderType::Exchange { + continue; + } + if let Some(sku_row) = sqlx::query("SELECT sku_category_id FROM sku_list WHERE id = ? LIMIT 1") + .bind(item.sku_id) + .fetch_optional(&mut *tx) + .await? + { + match inventory.get_mut(warehouse_id, item.sku_id, tx).await? { + Some(product) => { + product.change(calc_quantity_by_order_type(product.latest_quantity(), item, order_type)); + products.insert(item.sku_id); + } + None => { + let produtc_quantity = calc_quantity_by_order_type(0, item, order_type); + add(warehouse_id, item.sku_id, sku_row.get("sku_category_id"), produtc_quantity, tx) + .await? + .expect("Can't add the new inventory!"); + } + } + } else { + bail!("SKU not found!") + } + } + for sku_id in products { + let vproduct = inventory.get_mut(warehouse_id, sku_id, tx).await?.expect("It must contain because called get_mut() before"); + let product = InventoryProduct { + warehouse_id, + sku_id, + sku_category_id: vproduct.sku_category_id, + quantity: vproduct.latest_quantity(), + }; + if update(product, tx).await?.is_none() { + warn!("Can't update the specified product by id {}!", sku_id); + bail!("Please ensure order is correct!"); + } + } + + Ok(()) +} diff --git a/server/crates/elerp_common/src/inventory_module/model.rs b/server/crates/elerp_common/src/inventory_module/model.rs new file mode 100644 index 0000000..8c064bf --- /dev/null +++ b/server/crates/elerp_common/src/inventory_module/model.rs @@ -0,0 +1,2 @@ +pub mod inventory; +pub mod virtual_inventory; diff --git a/server/src/erp/inventory_module/model/inventory.rs b/server/crates/elerp_common/src/inventory_module/model/inventory.rs similarity index 97% rename from server/src/erp/inventory_module/model/inventory.rs rename to server/crates/elerp_common/src/inventory_module/model/inventory.rs index cb6c3fc..1f45b04 100644 --- a/server/src/erp/inventory_module/model/inventory.rs +++ b/server/crates/elerp_common/src/inventory_module/model/inventory.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::erp::util::{get_sort_col_str, get_sorter_str}; +use crate::sql::{get_sort_col_str, get_sorter_str}; #[derive(Debug, Serialize, Deserialize, ToSchema, FromRow, IntoParams, Clone)] pub struct InventoryProduct { diff --git a/server/crates/elerp_common/src/inventory_module/model/virtual_inventory.rs b/server/crates/elerp_common/src/inventory_module/model/virtual_inventory.rs new file mode 100644 index 0000000..cb2c422 --- /dev/null +++ b/server/crates/elerp_common/src/inventory_module/model/virtual_inventory.rs @@ -0,0 +1,66 @@ +use ahash::{HashMap, HashMapExt}; +use anyhow::Result; +use sqlx::{Row, SqliteConnection}; + +#[derive(Debug, Clone)] +pub struct VirtualInventory { + inventory: HashMap<(i64, i64), Option>, +} + +#[derive(Debug, Clone)] +pub struct VirtualInventoryProduct { + pub sku_category_id: i64, + quantity: i64, + latest_quantity: i64, +} + +impl VirtualInventoryProduct { + pub fn quantity(&self) -> i64 { + self.quantity + } + + pub fn latest_quantity(&self) -> i64 { + self.latest_quantity + } + + pub fn change(&mut self, v: i64) -> i64 { + self.latest_quantity = v; + self.latest_quantity + } +} + +impl VirtualInventory { + pub fn new(capicity: usize) -> Self { + Self { + inventory: HashMap::with_capacity(capicity), + } + } + + pub async fn get_mut( + &mut self, + warehouse_id: i64, + sku_id: i64, + tx: &mut SqliteConnection, + ) -> Result> { + Ok(if self.inventory.contains_key(&(warehouse_id, sku_id)) { + let p = self.inventory.get_mut(&(warehouse_id, sku_id)).unwrap(); + p.as_mut() + } else { + let p =sqlx::query("SELECT sku_category_id, quantity FROM inventory WHERE warehouse_id = ? AND sku_id = ? LIMIT 1").bind(warehouse_id).bind(sku_id) + .fetch_optional(&mut *tx).await? + .map(|p| { + let quantity = p.get("quantity"); + VirtualInventoryProduct { + sku_category_id: p.get("sku_category_id"), + quantity, + latest_quantity: quantity, + } + }); + self.inventory.insert((warehouse_id, sku_id), p); + self.inventory + .get_mut(&(warehouse_id, sku_id)) + .unwrap() + .as_mut() + }) + } +} diff --git a/server/crates/elerp_common/src/lib.rs b/server/crates/elerp_common/src/lib.rs new file mode 100644 index 0000000..61fa834 --- /dev/null +++ b/server/crates/elerp_common/src/lib.rs @@ -0,0 +1,212 @@ +use ahash::HashSet; + +pub mod area_module; +pub mod config; +pub mod guest_order_module; +pub mod inventory_module; +pub mod meta; +pub mod model; +pub mod order_category_module; +pub mod order_module; +pub mod order_payment_module; +pub mod person_module; +pub mod sku_category_module; +pub mod sku_module; +pub mod statistical_module; +pub mod user_system; +pub mod warehouse_module; + +pub fn get_test_config() -> config::AppConfig { + use config::AppConfig; + use tempfile::tempdir; + + use crate::config::{Limit, Web, TLS, WS}; + + let tmp_dir = tempdir().expect("Get temp directory failed!"); + AppConfig { + data_path: tmp_dir.into_path(), + web: Web::default(), + host: "0.0.0.0".to_owned(), + port: 3345, + limit: Limit { + areas: 9, + persons: 9, + users: 9, + warehouses: 9, + sku_categories: 9, + skus: 9, + order_categories: 9, + order_payments: 9, + orders: 9, + guest_orders: 9, + statistics: 9, + }, + tls: TLS::default(), + ws: WS::default(), + } +} + +pub fn i64_safe_max() -> i64 { + i64::MAX - 1 +} + +pub fn set_to_string(set: &HashSet, sep: &str) -> String { + set.iter().map(|n| n.to_string()).collect::>().join(sep) +} + +pub mod sql { + use std::{borrow::Cow, sync::OnceLock}; + + use ahash::HashSet; + use anyhow::Result; + use futures::TryStreamExt; + use regex::Regex; + use sqlx::{sqlite::SqliteRow, Encode, FromRow, Sqlite, SqliteConnection, Type}; + + const STANDARD_ID_NUM: i64 = 10000; + static RE: OnceLock = OnceLock::new(); + + pub fn get_search_where_condition(col: &str, query: &str) -> String { + let re: &Regex = RE.get_or_init(|| Regex::new(r"[\s+\(\)\-\:\@()]").unwrap()); + let mut tmp = [0u8; 4]; + let qs: Vec<&str> = query.trim().split(|a: char| re.is_match(a.encode_utf8(&mut tmp))).collect(); + let mut conditions = Vec::with_capacity(qs.len()); + for q in qs { + conditions.push(format!("{col} LIKE '%{q}%'")); + } + conditions.join(" AND ").into() + } + + pub fn get_sorter_str(sorter: &str) -> &'static str { + if sorter.contains(":descend") { + "DESC" + } else { + "ASC" + } + } + + pub fn get_sort_col_str(col: &str) -> String { + col.replace(":ascend", "").replace(":descend", "").into() + } + + pub fn eq_or_not(reverse: Option<&HashSet>, col: &str) -> &'static str { + match reverse { + Some(reverse) => { + if reverse.contains(col) { + "<>" + } else { + "=" + } + } + None => "=", + } + } + + pub fn in_or_not(reverse: Option<&HashSet>, col: &str) -> &'static str { + match reverse { + Some(reverse) => { + if reverse.contains(col) { + " NOT IN " + } else { + " IN " + } + } + None => " IN ", + } + } + + pub fn exists_or_not(reverse: Option<&HashSet>, col: &str) -> &'static str { + match reverse { + Some(reverse) => { + if reverse.contains(col) { + " NOT EXISTS " + } else { + " EXISTS " + } + } + None => " EXISTS ", + } + } + + pub fn like_or_not(reverse: Option<&HashSet>, col: &str) -> &'static str { + match reverse { + Some(reverse) => { + if reverse.contains(col) { + " NOT LIKE " + } else { + " LIKE " + } + } + None => " LIKE ", + } + } + + pub async fn row_is_duplicate_col_in_table(col: &str, prev: Option, table_name: &str, col_name: &str, tx: &mut SqliteConnection) -> Result { + let q1 = format!("SELECT id FROM {table_name} WHERE {col_name}=? AND id<>? LIMIT 1"); + let q2 = format!("SELECT id FROM {table_name} WHERE {col_name}=? LIMIT 1"); + let mut r = if let Some(prev) = prev { + sqlx::query(&q1).bind(col).bind(prev).fetch(&mut *tx) + } else { + sqlx::query(&q2).bind(col).fetch(&mut *tx) + }; + Ok(r.try_next().await?.is_some()) + } + + pub async fn get_row_from_table(table_name: &str, col_name: &str, col_value: V, tx: &mut SqliteConnection) -> Result> + where + for<'q> V: 'q + Send + Encode<'q, Sqlite> + Type, + for<'r> T: FromRow<'r, SqliteRow> + Unpin + Send, + { + let q = format!("SELECT * FROM {table_name} WHERE {col_name} = ?"); + let mut r = sqlx::query(&q).bind(col_value).fetch(&mut *tx); + Ok(if let Some(row) = r.try_next().await? { + let v = T::from_row(&row)?; + Some(v) + } else { + None + }) + } + + pub async fn is_exists_in_table(table_name: &str, col_name: &str, col_value: V, tx: &mut SqliteConnection) -> Result + where + for<'q> V: 'q + Send + Encode<'q, Sqlite> + Type, + { + let q = format!("SELECT {col_name} FROM {table_name} WHERE {col_name} = ?"); + let mut r = sqlx::query(&q).bind(col_value).fetch(&mut *tx); + Ok(r.try_next().await?.is_some()) + } + + pub fn rows_to_objects<'a, T>(rows: Vec) -> Result> + where + for<'r> T: FromRow<'r, SqliteRow> + Unpin + Send + 'a, + { + let mut arr = Vec::with_capacity(rows.len()); + for row in rows { + arr.push(T::from_row(&row)?) + } + + Ok(arr) + } + + pub async fn remove_row_from_table(row_id: i64, table_name: &str, tx: &mut SqliteConnection) -> Result { + let q = format!("DELETE FROM {table_name} WHERE id = ?"); + let r = sqlx::query(&q).bind(row_id).execute(&mut *tx).await?; + Ok(r.rows_affected() == 1) + } + + pub async fn try_set_standard_id(creation_id: i64, table_name: &str, tx: &mut SqliteConnection) -> Result { + if creation_id == 1 { + let nid = STANDARD_ID_NUM + 1; + sqlx::query(&Cow::Owned(format!("UPDATE {table_name} SET id = {nid} WHERE id = {creation_id}"))) + .execute(&mut *tx) + .await?; + Ok(nid) + } else { + Ok(creation_id as _) + } + } + + pub fn get_standard_id(id: i64) -> i64 { + STANDARD_ID_NUM + id + } +} diff --git a/server/src/meta.rs b/server/crates/elerp_common/src/meta.rs similarity index 94% rename from server/src/meta.rs rename to server/crates/elerp_common/src/meta.rs index 368a4d8..56ddb96 100644 --- a/server/src/meta.rs +++ b/server/crates/elerp_common/src/meta.rs @@ -45,6 +45,12 @@ pub struct MetaInfo { pub cmd: Commands, } +impl MetaInfo { + pub fn new() -> Self { + Self::parse() + } +} + #[derive(Subcommand, Debug, Clone)] pub enum Commands { Update, diff --git a/server/src/public_system/model.rs b/server/crates/elerp_common/src/model.rs similarity index 89% rename from server/src/public_system/model.rs rename to server/crates/elerp_common/src/model.rs index 2d92e87..a7d074e 100644 --- a/server/src/public_system/model.rs +++ b/server/crates/elerp_common/src/model.rs @@ -1,6 +1,7 @@ pub mod list_slice; pub mod pagination; pub mod web_socket_flags; +pub mod action_type; pub use list_slice::ListSlice; pub use pagination::Pagination; diff --git a/server/crates/elerp_common/src/model/action_type.rs b/server/crates/elerp_common/src/model/action_type.rs new file mode 100644 index 0000000..c81ca1e --- /dev/null +++ b/server/crates/elerp_common/src/model/action_type.rs @@ -0,0 +1,8 @@ + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ActionType { + General(i64), + GeneralAllowed(i64), + Admin, + System, +} \ No newline at end of file diff --git a/server/src/public_system/model/list_slice.rs b/server/crates/elerp_common/src/model/list_slice.rs similarity index 100% rename from server/src/public_system/model/list_slice.rs rename to server/crates/elerp_common/src/model/list_slice.rs diff --git a/server/src/public_system/model/pagination.rs b/server/crates/elerp_common/src/model/pagination.rs similarity index 100% rename from server/src/public_system/model/pagination.rs rename to server/crates/elerp_common/src/model/pagination.rs diff --git a/server/src/public_system/model/web_socket_flags.rs b/server/crates/elerp_common/src/model/web_socket_flags.rs similarity index 100% rename from server/src/public_system/model/web_socket_flags.rs rename to server/crates/elerp_common/src/model/web_socket_flags.rs diff --git a/server/crates/elerp_common/src/order_category_module.rs b/server/crates/elerp_common/src/order_category_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/order_category_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/crates/elerp_common/src/order_category_module/model.rs b/server/crates/elerp_common/src/order_category_module/model.rs new file mode 100644 index 0000000..8028b47 --- /dev/null +++ b/server/crates/elerp_common/src/order_category_module/model.rs @@ -0,0 +1 @@ +pub mod order_category; diff --git a/server/src/erp/order_category_module/model/order_category.rs b/server/crates/elerp_common/src/order_category_module/model/order_category.rs similarity index 95% rename from server/src/erp/order_category_module/model/order_category.rs rename to server/crates/elerp_common/src/order_category_module/model/order_category.rs index 43bc767..733d854 100644 --- a/server/src/erp/order_category_module/model/order_category.rs +++ b/server/crates/elerp_common/src/order_category_module/model/order_category.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::erp::util::{get_sort_col_str, get_sorter_str, get_search_where_condition}; +use crate::sql::{get_search_where_condition, get_sort_col_str, get_sorter_str}; #[derive(Debug, Serialize, Deserialize, ToSchema, FromRow, Clone)] pub struct OrderCategory { diff --git a/server/crates/elerp_common/src/order_module.rs b/server/crates/elerp_common/src/order_module.rs new file mode 100644 index 0000000..280bee3 --- /dev/null +++ b/server/crates/elerp_common/src/order_module.rs @@ -0,0 +1,188 @@ +use crate::{inventory_module, sql}; +use ahash::{HashMap, HashMapExt}; +use anyhow::{bail, Result}; +use futures::TryStreamExt; +use sqlx::{QueryBuilder, Row, SqliteConnection}; + +use self::model::{ + check_order_result::{CheckOrderResult, ItemNotAvailable}, + order::{Order, OrderItem, OrderPaymentStatus, OrderType}, +}; + +pub mod model; + +pub async fn check(order: &Order, fast_check: bool, tx: &mut SqliteConnection) -> Result { + let mut items_not_available = Vec::new(); + let items = if let Some(items) = order.items.as_ref() { + items + } else { + return Ok(CheckOrderResult { items_not_available: vec![] }); + }; + let mut item_map = HashMap::with_capacity(items.len()); + for item in items { + if !item.exchanged { + item_map.entry(item.sku_id).and_modify(|q| *q += item.quantity).or_insert(item.quantity); + } + } + let mut inventory = { inventory_module::get_virtual(items.len()) }; + match order.order_type { + OrderType::Exchange | OrderType::StockOut => { + for (sku_id, require_quantity) in item_map { + let (latest_quantity, actual_quantity) = inventory + .get_mut(order.warehouse_id, sku_id, tx) + .await? + .map(|p| (p.change(p.latest_quantity() - require_quantity), p.quantity())) + .unwrap_or((0 - require_quantity, 0)); + if latest_quantity < 0 { + items_not_available.push(ItemNotAvailable { + sku_id, + require_quantity, + actual_quantity, + }); + if fast_check { + return Ok(CheckOrderResult { items_not_available }); + } + } + } + } + OrderType::StockIn | OrderType::Return | OrderType::Calibration | OrderType::CalibrationStrict => (), + OrderType::Verification => { + for (sku_id, require_quantity) in item_map { + let actual_quantity = inventory.get_mut(order.warehouse_id, sku_id, tx).await?.map(|p| p.quantity()).unwrap_or(0); + if actual_quantity != require_quantity { + items_not_available.push(ItemNotAvailable { + sku_id, + require_quantity, + actual_quantity, + }); + if fast_check { + return Ok(CheckOrderResult { items_not_available }); + } + } + } + } + OrderType::VerificationStrict => { + let mut item_ids = Vec::with_capacity(item_map.len()); + for (sku_id, require_quantity) in item_map { + item_ids.push(sku_id); + let actual_quantity = inventory.get_mut(order.warehouse_id, sku_id, tx).await?.map(|p| p.quantity()).unwrap_or(0); + if actual_quantity != require_quantity { + items_not_available.push(ItemNotAvailable { + sku_id, + require_quantity, + actual_quantity, + }); + if fast_check { + return Ok(CheckOrderResult { items_not_available }); + } + } + } + let ids = item_ids.into_iter().map(|n| n.to_string()).collect::>().join(","); + + if fast_check { + if let Some(row) = sqlx::query(&format!( + "SELECT sku_id, quantity FROM inventory WHERE warehouse_id=? AND sku_id NOT IN ({ids}) AND quantity <> 0 LIMIT 1" + )) + .bind(order.warehouse_id) + .fetch(&mut *tx) + .try_next() + .await? + { + items_not_available.push(ItemNotAvailable { + sku_id: row.get("sku_id"), + require_quantity: 0, + actual_quantity: row.get("quantity"), + }); + return Ok(CheckOrderResult { items_not_available }); + } + } else { + let q = format!("SELECT sku_id, quantity FROM inventory WHERE warehouse_id=? AND sku_id NOT IN ({ids}) AND quantity <> 0"); + let mut r = sqlx::query(&q).bind(order.warehouse_id).fetch(&mut *tx); + while let Some(row) = r.try_next().await? { + items_not_available.push(ItemNotAvailable { + sku_id: row.get("sku_id"), + require_quantity: 0, + actual_quantity: row.get("quantity"), + }); + } + } + } + } + Ok(CheckOrderResult { items_not_available }) +} + +fn calc_total_amount(items: &Vec) -> f64 { + let mut total: f64 = 0.0; + for item in items.iter() { + if item.exchanged { + continue; + } + total += (item.quantity as f64) * item.price; + } + total +} + +pub async fn add(mut order: Order, tx: &mut SqliteConnection) -> Result { + let items = order.items.as_ref(); + let total_amount = if let Some(items) = items { + inventory_module::change(order.warehouse_id, items, order.order_type, tx).await?; + calc_total_amount(items) + } else { + 0.0 + }; + let order_payment_status = if order.order_type == OrderType::StockOut && total_amount > 0.0 { + OrderPaymentStatus::Unsettled + } else { + OrderPaymentStatus::None + }; + let r = sqlx::query("INSERT INTO orders (from_guest_order_id, created_by_user_id, updated_by_user_id, warehouse_id, currency, total_amount, person_related_id, person_in_charge_id, date, last_updated_date, description, order_type, order_category_id, total_amount_settled, order_payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + .bind(order.from_guest_order_id) + .bind(order.created_by_user_id) + .bind(order.updated_by_user_id) + .bind(order.warehouse_id) + .bind(&order.currency) + .bind(total_amount) + .bind(order.person_related_id) + .bind(order.person_in_charge_id) + .bind(order.date) + .bind(order.last_updated_date) + .bind(&order.description) + .bind(&order.order_type) + .bind(sql::get_standard_id(1)) + .bind(0) + .bind(order_payment_status) + .execute(&mut *tx) + .await?; + if r.rows_affected() != 1 { + bail!("Can't insert the order to history!"); + } + + order.id = sql::try_set_standard_id(r.last_insert_rowid(), "orders", tx).await?; + order.total_amount = total_amount; + order.total_amount_settled = 0.0; + order.order_payment_status = order_payment_status; + + add_order_items(&order, tx).await?; + + Ok(order) +} + +async fn add_order_items(order: &Order, tx: &mut SqliteConnection) -> Result<()> { + let items = order.items.as_ref(); + if items.is_none() || items.as_ref().unwrap().len() == 0 { + return Ok(()); + } + let mut query_builder = QueryBuilder::new("INSERT INTO order_items (order_id, sku_id, sku_category_id, quantity, price, exchanged, amount) "); + query_builder.push_values(items.unwrap(), |mut b, item| { + b.push_bind(order.id) + .push_bind(item.sku_id) + .push(format!("(SELECT sku_category_id FROM sku_list WHERE id = {})", item.sku_id)) + .push_bind(item.quantity) + .push_bind(item.price) + .push_bind(item.exchanged) + .push_bind(item.quantity as f64 * item.price); + }); + let query = query_builder.build(); + query.execute(&mut *tx).await?; + Ok(()) +} diff --git a/server/crates/elerp_common/src/order_module/model.rs b/server/crates/elerp_common/src/order_module/model.rs new file mode 100644 index 0000000..4e60156 --- /dev/null +++ b/server/crates/elerp_common/src/order_module/model.rs @@ -0,0 +1,2 @@ +pub mod check_order_result; +pub mod order; diff --git a/server/src/erp/order_module/model/check_order_result.rs b/server/crates/elerp_common/src/order_module/model/check_order_result.rs similarity index 100% rename from server/src/erp/order_module/model/check_order_result.rs rename to server/crates/elerp_common/src/order_module/model/check_order_result.rs diff --git a/server/src/erp/order_module/model/order.rs b/server/crates/elerp_common/src/order_module/model/order.rs similarity index 98% rename from server/src/erp/order_module/model/order.rs rename to server/crates/elerp_common/src/order_module/model/order.rs index a1e5464..263e80a 100644 --- a/server/src/erp/order_module/model/order.rs +++ b/server/crates/elerp_common/src/order_module/model/order.rs @@ -5,8 +5,8 @@ use strum::AsRefStr; use utoipa::{IntoParams, ToSchema}; use crate::{ - erp::util::{eq_or_not, exists_or_not, get_sort_col_str, get_sorter_str, in_or_not, like_or_not}, - myhelper::set_to_string, + set_to_string, + sql::{eq_or_not, exists_or_not, get_sort_col_str, get_sorter_str, in_or_not, like_or_not}, }; #[derive( @@ -153,7 +153,7 @@ pub struct OrderItem { pub exchanged: bool, } -#[derive(Debug, Deserialize, ToSchema, IntoParams)] +#[derive(Debug, Default, Deserialize, ToSchema, IntoParams)] pub struct GetOrdersQuery { pub id: Option, pub created_by_user_id: Option, diff --git a/server/crates/elerp_common/src/order_payment_module.rs b/server/crates/elerp_common/src/order_payment_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/order_payment_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/crates/elerp_common/src/order_payment_module/model.rs b/server/crates/elerp_common/src/order_payment_module/model.rs new file mode 100644 index 0000000..5be0f4f --- /dev/null +++ b/server/crates/elerp_common/src/order_payment_module/model.rs @@ -0,0 +1 @@ +pub mod order_payment; diff --git a/server/src/erp/order_payment_module/model/order_payment.rs b/server/crates/elerp_common/src/order_payment_module/model/order_payment.rs similarity index 97% rename from server/src/erp/order_payment_module/model/order_payment.rs rename to server/crates/elerp_common/src/order_payment_module/model/order_payment.rs index 396d27e..6baf20b 100644 --- a/server/src/erp/order_payment_module/model/order_payment.rs +++ b/server/crates/elerp_common/src/order_payment_module/model/order_payment.rs @@ -1,13 +1,9 @@ use ahash::HashSet; +use crate::{set_to_string, sql::{get_sort_col_str, get_sorter_str}}; use serde::{Deserialize, Serialize}; use sqlx::prelude::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::{ - erp::util::{get_sort_col_str, get_sorter_str}, - myhelper::set_to_string, -}; - #[derive(Debug, Serialize, Deserialize, ToSchema, Clone, IntoParams, FromRow)] pub struct OrderPayment { #[serde(default)] diff --git a/server/crates/elerp_common/src/person_module.rs b/server/crates/elerp_common/src/person_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/person_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/crates/elerp_common/src/person_module/model.rs b/server/crates/elerp_common/src/person_module/model.rs new file mode 100644 index 0000000..4a45102 --- /dev/null +++ b/server/crates/elerp_common/src/person_module/model.rs @@ -0,0 +1 @@ +pub mod person; diff --git a/server/src/erp/person_module/model/person.rs b/server/crates/elerp_common/src/person_module/model/person.rs similarity index 95% rename from server/src/erp/person_module/model/person.rs rename to server/crates/elerp_common/src/person_module/model/person.rs index 413f75d..f771531 100644 --- a/server/src/erp/person_module/model/person.rs +++ b/server/crates/elerp_common/src/person_module/model/person.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use sqlx::prelude::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::erp::util::{get_search_where_condition, get_sort_col_str, get_sorter_str}; +use crate::sql::{get_search_where_condition, get_sort_col_str, get_sorter_str}; #[derive(Debug, Deserialize, Serialize, ToSchema, FromRow)] pub struct Person { @@ -27,7 +27,7 @@ pub struct Person { pub text_color: Option, } -#[derive(Debug, Deserialize, ToSchema, IntoParams)] +#[derive(Debug, Default, Deserialize, ToSchema, IntoParams)] pub struct GetPersonsQuery { pub id: Option, pub name: Option, diff --git a/server/crates/elerp_common/src/sku_category_module.rs b/server/crates/elerp_common/src/sku_category_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/sku_category_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/crates/elerp_common/src/sku_category_module/model.rs b/server/crates/elerp_common/src/sku_category_module/model.rs new file mode 100644 index 0000000..ad4238c --- /dev/null +++ b/server/crates/elerp_common/src/sku_category_module/model.rs @@ -0,0 +1 @@ +pub mod sku_category; diff --git a/server/src/erp/sku_category_module/model/sku_category.rs b/server/crates/elerp_common/src/sku_category_module/model/sku_category.rs similarity index 95% rename from server/src/erp/sku_category_module/model/sku_category.rs rename to server/crates/elerp_common/src/sku_category_module/model/sku_category.rs index 9536810..9d6e5d8 100644 --- a/server/src/erp/sku_category_module/model/sku_category.rs +++ b/server/crates/elerp_common/src/sku_category_module/model/sku_category.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::erp::util::{get_search_where_condition, get_sort_col_str, get_sorter_str}; +use crate::sql::{get_search_where_condition, get_sort_col_str, get_sorter_str}; #[derive(Debug, Serialize, Deserialize, ToSchema, FromRow, Clone)] pub struct SKUCategory { diff --git a/server/crates/elerp_common/src/sku_module.rs b/server/crates/elerp_common/src/sku_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/sku_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/crates/elerp_common/src/sku_module/model.rs b/server/crates/elerp_common/src/sku_module/model.rs new file mode 100644 index 0000000..551722d --- /dev/null +++ b/server/crates/elerp_common/src/sku_module/model.rs @@ -0,0 +1 @@ +pub mod sku; diff --git a/server/src/erp/sku_module/model/sku.rs b/server/crates/elerp_common/src/sku_module/model/sku.rs similarity index 96% rename from server/src/erp/sku_module/model/sku.rs rename to server/crates/elerp_common/src/sku_module/model/sku.rs index cc93ca4..1f5f116 100644 --- a/server/src/erp/sku_module/model/sku.rs +++ b/server/crates/elerp_common/src/sku_module/model/sku.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use sqlx::prelude::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::erp::util::{get_search_where_condition, get_sort_col_str, get_sorter_str}; +use crate::sql::{get_search_where_condition, get_sort_col_str, get_sorter_str}; #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Serialize, Deserialize, ToSchema, FromRow)] diff --git a/server/crates/elerp_common/src/statistical_module.rs b/server/crates/elerp_common/src/statistical_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/statistical_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/src/erp/statistical_module/model.rs b/server/crates/elerp_common/src/statistical_module/model.rs similarity index 100% rename from server/src/erp/statistical_module/model.rs rename to server/crates/elerp_common/src/statistical_module/model.rs diff --git a/server/src/erp/statistical_module/model/statistical_data.rs b/server/crates/elerp_common/src/statistical_module/model/statistical_data.rs similarity index 98% rename from server/src/erp/statistical_module/model/statistical_data.rs rename to server/crates/elerp_common/src/statistical_module/model/statistical_data.rs index 17bb2cb..dea1675 100644 --- a/server/src/erp/statistical_module/model/statistical_data.rs +++ b/server/crates/elerp_common/src/statistical_module/model/statistical_data.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use sqlx::prelude::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::erp::order_module::model::{GetOrdersQuery, OrderCurrency}; +use crate::order_module::model::order::{GetOrdersQuery, OrderCurrency}; #[derive(Debug, Serialize, Deserialize, ToSchema, FromRow, Clone, PartialOrd)] pub struct PopularSKU { diff --git a/server/crates/elerp_common/src/user_system.rs b/server/crates/elerp_common/src/user_system.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/user_system.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/src/user_system/models.rs b/server/crates/elerp_common/src/user_system/model.rs similarity index 100% rename from server/src/user_system/models.rs rename to server/crates/elerp_common/src/user_system/model.rs diff --git a/server/src/user_system/models/user_configure.rs b/server/crates/elerp_common/src/user_system/model/user_configure.rs similarity index 92% rename from server/src/user_system/models/user_configure.rs rename to server/crates/elerp_common/src/user_system/model/user_configure.rs index 39fff75..def03fd 100644 --- a/server/src/user_system/models/user_configure.rs +++ b/server/crates/elerp_common/src/user_system/model/user_configure.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::erp::order_module::model::{OrderCurrency, OrderType}; +use crate::order_module::model::order::{OrderCurrency, OrderType}; #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct UserConfigureDefaults { diff --git a/server/src/user_system/models/user_info.rs b/server/crates/elerp_common/src/user_system/model/user_info.rs similarity index 98% rename from server/src/user_system/models/user_info.rs rename to server/crates/elerp_common/src/user_system/model/user_info.rs index 45ccd45..56bca77 100644 --- a/server/src/user_system/models/user_info.rs +++ b/server/crates/elerp_common/src/user_system/model/user_info.rs @@ -3,7 +3,7 @@ use sqlx::prelude::FromRow; use strum::AsRefStr; use utoipa::{IntoParams, ToSchema}; -use crate::erp::ActionType; +use crate::model::action_type::ActionType; #[derive( Debug, diff --git a/server/src/user_system/models/user_permission.rs b/server/crates/elerp_common/src/user_system/model/user_permission.rs similarity index 100% rename from server/src/user_system/models/user_permission.rs rename to server/crates/elerp_common/src/user_system/model/user_permission.rs diff --git a/server/crates/elerp_common/src/warehouse_module.rs b/server/crates/elerp_common/src/warehouse_module.rs new file mode 100644 index 0000000..65880be --- /dev/null +++ b/server/crates/elerp_common/src/warehouse_module.rs @@ -0,0 +1 @@ +pub mod model; diff --git a/server/src/erp/warehouse_module/model.rs b/server/crates/elerp_common/src/warehouse_module/model.rs similarity index 100% rename from server/src/erp/warehouse_module/model.rs rename to server/crates/elerp_common/src/warehouse_module/model.rs index fb35515..a8d3f78 100644 --- a/server/src/erp/warehouse_module/model.rs +++ b/server/crates/elerp_common/src/warehouse_module/model.rs @@ -1,2 +1,2 @@ -pub mod warehouse; pub mod fn_argument; +pub mod warehouse; diff --git a/server/src/erp/warehouse_module/model/fn_argument.rs b/server/crates/elerp_common/src/warehouse_module/model/fn_argument.rs similarity index 88% rename from server/src/erp/warehouse_module/model/fn_argument.rs rename to server/crates/elerp_common/src/warehouse_module/model/fn_argument.rs index 52d6d86..5df1266 100644 --- a/server/src/erp/warehouse_module/model/fn_argument.rs +++ b/server/crates/elerp_common/src/warehouse_module/model/fn_argument.rs @@ -1,4 +1,4 @@ -use crate::user_system::models::user_info::UserInfo; +use crate::user_system::model::user_info::UserInfo; pub enum UserInfoID<'a> { InfoRef(&'a UserInfo), diff --git a/server/src/erp/warehouse_module/model/warehouse.rs b/server/crates/elerp_common/src/warehouse_module/model/warehouse.rs similarity index 97% rename from server/src/erp/warehouse_module/model/warehouse.rs rename to server/crates/elerp_common/src/warehouse_module/model/warehouse.rs index f1a4779..610d179 100644 --- a/server/src/erp/warehouse_module/model/warehouse.rs +++ b/server/crates/elerp_common/src/warehouse_module/model/warehouse.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use utoipa::{IntoParams, ToSchema}; -use crate::erp::util::{get_search_where_condition, get_sort_col_str, get_sorter_str}; +use crate::sql::{get_search_where_condition, get_sort_col_str, get_sorter_str}; #[derive(Debug, Serialize, Deserialize, ToSchema, FromRow)] pub struct Warehouse { diff --git a/server/crates/elerp_service/Cargo.toml b/server/crates/elerp_service/Cargo.toml new file mode 100644 index 0000000..ffe937d --- /dev/null +++ b/server/crates/elerp_service/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "elerp_service" +version = "0.1.0" +edition = "2021" + +[dependencies] +tower-http = { workspace = true } +axum = { workspace = true } +axum-server = { workspace = true } +axum-extra = { workspace = true } +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +user_system = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa = { workspace = true } +utoipa-rapidoc = { workspace = true } +serde_qs = { workspace = true } +strum = { workspace = true } +tokio = { workspace = true } +tokio-util = { workspace = true } +rcgen = { workspace = true } +futures = { workspace = true } + +area_module = { workspace = true } +person_module = { workspace = true } +warehouse_module = { workspace = true } +sku_module = { workspace = true } +sku_category_module = { workspace = true } +order_module = { workspace = true } +order_category_module = { workspace = true } +order_payment_module = { workspace = true } +guest_order_module = { workspace = true } +statistical_module = { workspace = true } +inventory_module = { workspace = true } \ No newline at end of file diff --git a/server/src/custom_error.rs b/server/crates/elerp_service/src/custom_error.rs similarity index 100% rename from server/src/custom_error.rs rename to server/crates/elerp_service/src/custom_error.rs diff --git a/server/src/erp.rs b/server/crates/elerp_service/src/erp.rs similarity index 52% rename from server/src/erp.rs rename to server/crates/elerp_service/src/erp.rs index 6060246..4e30fde 100644 --- a/server/src/erp.rs +++ b/server/crates/elerp_service/src/erp.rs @@ -1,37 +1,20 @@ use std::sync::Arc; -use crate::public_system::PublicSystem; - -use self::{ - area_module::AreaModule, dependency::ModuleDependency, guest_order_module::GuestOrderModule, inventory_module::InventoryModule, order_module::OrderModule, order_payment_module::OrderPaymentModule, order_category_module::OrderCategoryModule, person_module::PersonModule, sku_category_module::SKUCategoryModule, sku_module::SKUModule, statistical_module::StatisticalModule, warehouse_module::WarehouseModule -}; - -pub mod area_module; -pub mod person_module; - -pub mod inventory_module; -pub mod order_module; -pub mod guest_order_module; -pub mod order_payment_module; -pub mod order_category_module; -pub mod sku_category_module; -pub mod sku_module; -pub mod statistical_module; -pub mod warehouse_module; - -pub mod dependency; -pub mod util; +use area_module::AreaModule; +use guest_order_module::GuestOrderModule; +use inventory_module::InventoryModule; +use order_category_module::OrderCategoryModule; +use order_module::OrderModule; +use order_payment_module::OrderPaymentModule; +use person_module::PersonModule; +use public_system::PublicSystem; +use sku_category_module::SKUCategoryModule; +use sku_module::SKUModule; +use statistical_module::StatisticalModule; +use warehouse_module::WarehouseModule; type Result = anyhow::Result; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ActionType { - General(i64), - GeneralAllowed(i64), - Admin, - System, -} - #[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone)] pub struct ERP { @@ -63,23 +46,6 @@ impl ERP { let inventory = Arc::new(InventoryModule::new(ps.clone()).await); let statistical = Arc::new(StatisticalModule::new(ps.clone()).await); - let dep = ModuleDependency { - area: area.clone(), - person: person.clone(), - warehouse: warehouse.clone(), - sku: sku.clone(), - sku_category: sku_category.clone(), - order: order.clone(), - guest_order: guest_order.clone(), - order_category: order_category.clone(), - order_payment: order_payment.clone(), - inventory: inventory.clone(), - statistical: statistical.clone(), - }; - order.set_dependency(dep.clone()).await; - guest_order.set_dependency(dep.clone()).await; - order_payment.set_dependency(dep.clone()).await; - inventory.set_dependency(dep.clone()).await; ERP { area, person, diff --git a/server/src/services/erp_service.rs b/server/crates/elerp_service/src/erp_service.rs similarity index 93% rename from server/src/services/erp_service.rs rename to server/crates/elerp_service/src/erp_service.rs index 95f91b7..e3b6d9e 100644 --- a/server/src/services/erp_service.rs +++ b/server/crates/elerp_service/src/erp_service.rs @@ -1,10 +1,4 @@ -use crate::{ - custom_error::{AppError, CustomErrorCode}, - erp::{ - area_module::model::{Area, GetAreasQuery}, guest_order_module::model::{GetGuestOrdersQuery, GuestOrder, GuestOrderConfirm, GuestOrderStatus}, inventory_module::model::{GetInventoryQuery, InventoryProduct}, order_module::model::{CheckOrderResult, GetOrdersQuery, Order, OrderItem, OrderType}, order_payment_module::model::{GetOrderPaymentsQuery, OrderPayment}, order_category_module::model::{GetOrderCategoryQuery, OrderCategory}, person_module::model::{GetPersonsQuery, Person}, sku_category_module::model::{GetSKUCategoriesQuery, SKUCategory}, sku_module::model::{GetSKUsQuery, SKU}, statistical_module::model::statistical_data::{GetStatisticalDataQuery, StatisticalData}, warehouse_module::model::{fn_argument::WarehouseIsFrom, warehouse::{GetWarehousesQuery, Warehouse, WarehouseToLinkQuery}}, ActionType - }, - public_system::model::{ListSlice, Pagination}, user_system::models::user_info::{UserInfo, UserType}, -}; +use crate::custom_error::{AppError, CustomErrorCode}; use axum::{ body::Body, @@ -14,6 +8,7 @@ use axum::{ routing::{delete, get, post}, Json, Router, }; +use elerp_common::{area_module::model::area::{Area, GetAreasQuery}, guest_order_module::model::guest_order::{GetGuestOrdersQuery, GuestOrder, GuestOrderConfirm, GuestOrderStatus}, inventory_module::model::inventory::{GetInventoryQuery, InventoryProduct}, model::{action_type::ActionType, ListSlice, Pagination, WebSocketFlags}, order_category_module::model::order_category::{GetOrderCategoryQuery, OrderCategory}, order_module::model::{check_order_result::CheckOrderResult, order::{GetOrdersQuery, Order, OrderItem, OrderPaymentStatus, OrderType}}, order_payment_module::model::order_payment::{GetOrderPaymentsQuery, OrderPayment}, person_module::model::person::{GetPersonsQuery, Person}, sku_category_module::model::sku_category::{GetSKUCategoriesQuery, SKUCategory}, sku_module::model::sku::{GetSKUsQuery, SKU}, statistical_module::model::statistical_data::{GetStatisticalDataQuery, StatisticalData}, user_system::model::user_info::{UserInfo, UserType}, warehouse_module::model::{fn_argument::WarehouseIsFrom, warehouse::{GetWarehousesQuery, Warehouse, WarehouseToLinkQuery}}}; use serde_qs::axum::QsQuery as Query; use sqlx::SqliteConnection; @@ -21,7 +16,7 @@ use tokio_util::io::ReaderStream; use utoipa::OpenApi; -use super::{models::{authenticated_user::AuthenticatedUser, clear_result::ClearResult}, AppState}; +use super::{model::{authenticated_user::AuthenticatedUser, clear_result::ClearResult}, AppState}; type Result = core::result::Result; @@ -329,7 +324,7 @@ async fn clear_persons( ids = s.erp.person.get_multiple_ids(&pagination, &q, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearPersons).await?; + s.ps.notice(WebSocketFlags::ClearPersons).await?; Ok(Json(ClearResult { success, failed, @@ -522,7 +517,7 @@ async fn clear_areas( ids = s.erp.area.get_multiple_ids(&pagination, &q, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearAreas).await?; + s.ps.notice(WebSocketFlags::ClearAreas).await?; Ok(Json(ClearResult { success, failed, @@ -726,7 +721,7 @@ async fn clear_warehouses( ids = s.erp.warehouse.get_multiple_ids(&pagination, &q, action, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearWarehouses).await?; + s.ps.notice(WebSocketFlags::ClearWarehouses).await?; Ok(Json(ClearResult { success, failed, @@ -1017,7 +1012,7 @@ async fn clear_order_categories( ids = s.erp.order_category.get_multiple_ids(&pagination, &q, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearOrderCategories).await?; + s.ps.notice(WebSocketFlags::ClearOrderCategories).await?; Ok(Json(ClearResult { success, failed, @@ -1121,13 +1116,13 @@ async fn check_order_payment(s: AppState, user: &UserInfo, op: &OrderPayment, tx } let status = s.erp.order.get_order_payment_status(op.order_id, tx).await?.unwrap(); match status { - crate::erp::order_module::model::OrderPaymentStatus::Settled => return AppError::custom( + OrderPaymentStatus::Settled => return AppError::custom( CustomErrorCode::OrderPaymentSettled, "Order payment is settled.", ).into_err(), - crate::erp::order_module::model::OrderPaymentStatus::Unsettled | - crate::erp::order_module::model::OrderPaymentStatus::PartialSettled => (), - crate::erp::order_module::model::OrderPaymentStatus::None => return AppError::custom( + OrderPaymentStatus::Unsettled | + OrderPaymentStatus::PartialSettled => (), + OrderPaymentStatus::None => return AppError::custom( CustomErrorCode::OrderPaymentIsNone, "Order payment is none.", ).into_err(), @@ -1234,7 +1229,7 @@ async fn clear_order_payments( ids = s.erp.order_payment.get_multiple_ids(&pagination, &q, action, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearOrderPayments).await?; + s.ps.notice(WebSocketFlags::ClearOrderPayments).await?; Ok(Json(ClearResult { success, failed, @@ -1312,7 +1307,7 @@ async fn check_order( ) -> Result> { let mut tx = s.ps.begin_tx(false).await?; check_order_and_preprocess(s.clone(), &authenticated, true, &mut order, tx.as_mut()).await?; - let r = s.erp.order.check(&order, false, tx.as_mut()).await?; + let r = elerp_common::order_module::check(&order, false, tx.as_mut()).await?; tx.commit().await?; Ok(Json(r)) } @@ -1438,62 +1433,39 @@ async fn confirm_guest_order( Json(body): Json, ) -> Result> { let mut tx = s.ps.begin_tx(true).await?; + if !s.erp.guest_order.is_exists(id, tx.as_mut()).await? { + return AppError::custom(CustomErrorCode::OrderNotFound, "Guest order is not found.").into_err(); + } - if let Some(mut guest_order) = s.erp.guest_order.get(id, tx.as_mut()).await? { - match headers.get("X-Sub-Authorization") { - Some(client_token) => { - if client_token != &guest_order.sub_token { - return AppError::custom(CustomErrorCode::NoPermission, "You no permission to confirm!").into_err(); - } - }, - None => { - return AppError::custom(CustomErrorCode::NoPermission, "You no permission to confirm!").into_err(); - } - }; - let user = match s.us.get_sub_token_owner(&guest_order.sub_token, tx.as_mut()).await? { - Some(v) => v, - None => { - return AppError::custom(CustomErrorCode::UserNotFound, "Can't found the owner of your token!").into_err(); - } - }; - match guest_order.guest_order_status { - GuestOrderStatus::Confirmed => { - return AppError::custom(CustomErrorCode::GuestOrderConfirmed, "Guest order is confirmed already!").into_err(); - }, - GuestOrderStatus::Expired => { - return AppError::custom(CustomErrorCode::GuestOrderExpired, "Guest order is expired already!").into_err(); - }, - GuestOrderStatus::Pending => () - }; - - guest_order.description = body.description; - guest_order.items = body.items; - - let mut order: Order = guest_order.into(); - let pici = order.person_in_charge_id; - s.erp.order.preprocess(&mut order, &user, true, pici); - - if order.items.is_none() || order.items.as_ref().unwrap().len() == 0 { - return AppError::custom(CustomErrorCode::OrderItemsIsEmpty, "Order items is empty!").into_err(); + let token = match headers.get("X-Sub-Authorization") { + Some(client_token) => { + let token = client_token.to_str().unwrap_or(""); + if !s.erp.guest_order.is_sub_token_same(id, token, tx.as_mut()).await? { + return AppError::custom(CustomErrorCode::NoPermission, "You no permission to confirm!").into_err(); } - - let mut result = GuestOrderConfirm { - check_result: s.erp.order.check(&order, false, tx.as_mut()).await?, - order: None, - }; - if result.check_result.items_not_available.len() == 0 { - order.from_guest_order_id = id; - let order = s.erp.order.add(order, tx.as_mut()).await?; - s.erp.guest_order.confirm(id, &order, tx.as_mut()).await?; - let go = s.erp.guest_order.get(id, tx.as_mut()).await?.expect("Get empty guest order when confirm!"); - tx.commit().await?; - result.order = Some(go); + token + }, + None => { + return AppError::custom(CustomErrorCode::NoPermission, "You no permission to confirm!").into_err(); } - Ok(Json(result)) - } else { - AppError::custom(CustomErrorCode::OrderNotFound, "Order is not found.").into_err() + }; + if s.us.get_sub_token_owner(token, tx.as_mut()).await?.is_none() { + return AppError::custom(CustomErrorCode::UserNotFound, "Can't found the owner of your token!").into_err(); } - + match s.erp.guest_order.get_status(id, tx.as_mut()).await? { + GuestOrderStatus::Confirmed => { + return AppError::custom(CustomErrorCode::GuestOrderConfirmed, "Guest order is confirmed already!").into_err(); + }, + GuestOrderStatus::Expired => { + return AppError::custom(CustomErrorCode::GuestOrderExpired, "Guest order is expired already!").into_err(); + }, + GuestOrderStatus::Pending => () +}; + +match s.erp.guest_order.confirm(id, body, tx.as_mut()).await? { + Some(result) => Ok(Json(result)), + None => AppError::custom(CustomErrorCode::OrderNotFound, "Order is not found.").into_err() +} } async fn remove_order_core(s: AppState, user: &UserInfo, id: i64, recall: bool, notice: bool, tx: &mut SqliteConnection)-> Result { @@ -1578,7 +1550,7 @@ async fn clear_orders( s.erp.order.recalc_all(None, None, None, action, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearOrders).await?; + s.ps.notice(WebSocketFlags::ClearOrders).await?; Ok(Json(ClearResult { success, failed, @@ -1671,7 +1643,7 @@ async fn clear_guest_orders( s.erp.order.recalc_all(None, None, None, action, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearOrders).await?; + s.ps.notice(WebSocketFlags::ClearOrders).await?; Ok(Json(ClearResult { success, failed, @@ -2064,7 +2036,7 @@ async fn clear_skus( ids = s.erp.sku.get_multiple_ids(&pagination, &q, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearSKUs).await?; + s.ps.notice(WebSocketFlags::ClearSKUs).await?; Ok(Json(ClearResult { success, failed, @@ -2255,7 +2227,7 @@ async fn clear_sku_categories( ids = s.erp.sku_category.get_multiple_ids(&pagination, &q, tx.as_mut()).await?; } tx.commit().await?; - s.ps.notice(crate::public_system::model::WebSocketFlags::ClearSKUCategories).await?; + s.ps.notice(WebSocketFlags::ClearSKUCategories).await?; Ok(Json(ClearResult { success, failed, diff --git a/server/src/services.rs b/server/crates/elerp_service/src/lib.rs similarity index 60% rename from server/src/services.rs rename to server/crates/elerp_service/src/lib.rs index 9d93be0..e25c4da 100644 --- a/server/src/services.rs +++ b/server/crates/elerp_service/src/lib.rs @@ -1,14 +1,18 @@ +pub mod custom_error; +pub mod erp; pub mod erp_service; -pub mod models; +pub mod model; pub mod user_service; pub mod ws_service; -use crate::{config::AppConfig, erp::ERP, public_system::PublicSystem, user_system::UserSystem}; use axum::{ extract::{FromRef, State}, Router, }; use axum_server::tls_rustls::RustlsConfig; +use elerp_common::config::AppConfig; +use erp::ERP; +use public_system::PublicSystem; use std::net::SocketAddr; use tokio::fs; use tower_http::{ @@ -16,6 +20,7 @@ use tower_http::{ services::{ServeDir, ServeFile}, }; use tracing::info; +use user_system::UserSystem; use utoipa::OpenApi; use utoipa_rapidoc::RapiDoc; @@ -32,34 +37,33 @@ impl FromRef> for AppState { } } -pub async fn serve(config: AppConfig, pool: sqlx::Pool) { +pub async fn update(config: AppConfig) -> bool { + PublicSystem::update(config).await +} + +pub async fn serve(config: AppConfig) { + if PublicSystem::update(config.clone()).await { + info!("Updated..."); + } info!("Using {:#?}", config); - let ps = PublicSystem::new(pool, config.clone()).await; + let ps = PublicSystem::new(config.clone()).await; let state = AppState { erp: ERP::new(ps.clone()).await, us: UserSystem::new(ps.clone()).await, ps, }; - let cors = CorsLayer::new() - .allow_origin(Any) - .allow_headers(Any) - .allow_methods(Any); + let cors = CorsLayer::new().allow_origin(Any).allow_headers(Any).allow_methods(Any); //initial_erp(erp.clone()).await; let mut erp_openapi = erp_service::ApiDoc::openapi(); let mut us_openapi = user_service::ApiDoc::openapi(); erp_openapi.servers = Some(vec![utoipa::openapi::Server::new("/api/erp")]); us_openapi.servers = Some(vec![utoipa::openapi::Server::new("/api/us")]); - let api_router = Router::new() - .nest("/erp", erp_service::get_services()) - .nest("/us", user_service::get_services()); - let serve_dir = ServeDir::new(&config.web.dist) - .not_found_service(ServeFile::new(&config.web.dist.join(&config.web.index))); + let api_router = Router::new().nest("/erp", erp_service::get_services()).nest("/us", user_service::get_services()); + let serve_dir = ServeDir::new(&config.web.dist).not_found_service(ServeFile::new(&config.web.dist.join(&config.web.index))); let app = Router::new() .nest_service("/", serve_dir) - .merge( - RapiDoc::with_openapi("/crm-api-docs/openapi.json", erp_openapi).path("/rapidoc/erp"), - ) + .merge(RapiDoc::with_openapi("/crm-api-docs/openapi.json", erp_openapi).path("/rapidoc/erp")) .merge(RapiDoc::with_openapi("/wms-api-docs/openapi.json", us_openapi).path("/rapidoc/us")) .nest("/api", api_router) .nest("/socket", ws_service::get_services()) @@ -70,24 +74,15 @@ pub async fn serve(config: AppConfig, pool: sqlx::Pool) { let enable_tls = { if config.tls.self_tls { - let subject_alt_names: &[_] = &[ - "localhost".to_string(), - "127.0.0.1".to_string(), - "0.0.0.0".to_string(), - ]; + let subject_alt_names: &[_] = &["localhost".to_string(), "127.0.0.1".to_string(), "0.0.0.0".to_string()]; let cert = rcgen::generate_simple_self_signed(subject_alt_names).unwrap(); - Some(( - cert.serialize_pem().unwrap().as_bytes().to_vec(), - cert.serialize_private_key_pem().as_bytes().to_vec(), - )) + Some((cert.serialize_pem().unwrap().as_bytes().to_vec(), cert.serialize_private_key_pem().as_bytes().to_vec())) } else if config.tls.cert.is_some() && config.tls.key.is_some() { let cert_path = config.tls.cert.as_ref().unwrap(); let key_path = config.tls.key.as_ref().unwrap(); Some(( fs::read(cert_path).await.expect("Can't read the cert path"), - fs::read(key_path) - .await - .expect("Can't read the private key path"), + fs::read(key_path).await.expect("Can't read the private key path"), )) } else { None @@ -95,13 +90,10 @@ pub async fn serve(config: AppConfig, pool: sqlx::Pool) { }; if let Some((cert, key)) = enable_tls { let rustls_config = RustlsConfig::from_pem(cert, key).await.unwrap(); - axum_server::bind_rustls( - format!("{}:{}", config.host, config.port).parse().unwrap(), - rustls_config, - ) - .serve(app.into_make_service_with_connect_info::()) - .await - .unwrap(); + axum_server::bind_rustls(format!("{}:{}", config.host, config.port).parse().unwrap(), rustls_config) + .serve(app.into_make_service_with_connect_info::()) + .await + .unwrap(); } else { axum_server::bind(format!("{}:{}", config.host, config.port).parse().unwrap()) .serve(app.into_make_service_with_connect_info::()) diff --git a/server/src/services/models.rs b/server/crates/elerp_service/src/model.rs similarity index 100% rename from server/src/services/models.rs rename to server/crates/elerp_service/src/model.rs diff --git a/server/src/services/models/authenticated_user.rs b/server/crates/elerp_service/src/model/authenticated_user.rs similarity index 96% rename from server/src/services/models/authenticated_user.rs rename to server/crates/elerp_service/src/model/authenticated_user.rs index 05441da..2a0148f 100644 --- a/server/src/services/models/authenticated_user.rs +++ b/server/crates/elerp_service/src/model/authenticated_user.rs @@ -3,21 +3,22 @@ use axum::{ extract::{FromRef, FromRequestParts, MatchedPath}, http::request::Parts, }; +use elerp_common::{ + model::action_type::ActionType, + user_system::model::{ + user_info::{UserInfo, UserType}, + user_permission::{ + ADD_ORDER, ADD_ORDER_PAYMENT, MANAGE_AREA, MANAGE_ORDER_CATEGORY, MANAGE_PERSON, MANAGE_SKU, MANAGE_SKU_CATEGORY, MANAGE_WAREHOUSE, UPDATE_REMOVE_ORDER, UPDATE_REMOVE_ORDER_PAYMENT + }, + }, +}; use serde::{Deserialize, Serialize}; use tracing::info; use utoipa::{IntoParams, ToSchema}; use crate::{ custom_error::{AppError, CustomErrorCode}, - services::AppState, - user_system::models::{ - user_info::{UserInfo, UserType}, - user_permission::{ - ADD_ORDER, ADD_ORDER_PAYMENT, MANAGE_AREA, MANAGE_ORDER_CATEGORY, MANAGE_PERSON, - MANAGE_SKU, MANAGE_SKU_CATEGORY, MANAGE_WAREHOUSE, UPDATE_REMOVE_ORDER, - UPDATE_REMOVE_ORDER_PAYMENT, - }, - }, + AppState, }; #[derive(Debug, Deserialize, IntoParams, ToSchema)] @@ -74,7 +75,7 @@ where let mut tx = app.ps.begin_tx(false).await?; let user = app .us - .token_to_user(auth, crate::erp::ActionType::System, tx.as_mut()) + .token_to_user(auth, ActionType::System, tx.as_mut()) .await?; if let Some(user) = user { if path.starts_with("/api/erp") diff --git a/server/src/services/models/clear_result.rs b/server/crates/elerp_service/src/model/clear_result.rs similarity index 100% rename from server/src/services/models/clear_result.rs rename to server/crates/elerp_service/src/model/clear_result.rs diff --git a/server/src/services/models/web_socket_flag_json.rs b/server/crates/elerp_service/src/model/web_socket_flag_json.rs similarity index 98% rename from server/src/services/models/web_socket_flag_json.rs rename to server/crates/elerp_service/src/model/web_socket_flag_json.rs index ae80e44..3bcead7 100644 --- a/server/src/services/models/web_socket_flag_json.rs +++ b/server/crates/elerp_service/src/model/web_socket_flag_json.rs @@ -1,7 +1,6 @@ +use elerp_common::model::WebSocketFlags; use serde::Serialize; -use crate::public_system::model::WebSocketFlags; - #[derive(Debug, Serialize)] pub struct WebSocketFlagsJson { flag: String, diff --git a/server/src/services/user_service.rs b/server/crates/elerp_service/src/user_service.rs similarity index 97% rename from server/src/services/user_service.rs rename to server/crates/elerp_service/src/user_service.rs index f7eebe9..ddce268 100644 --- a/server/src/services/user_service.rs +++ b/server/crates/elerp_service/src/user_service.rs @@ -4,21 +4,15 @@ use axum::{ routing::{delete, get, post}, Json, Router, }; +use elerp_common::{model::{ListSlice, Pagination}, user_system::model::{user_configure::UserConfigure, user_info::{GetUsersQuery, UserInfo, UserType}}}; use serde_qs::axum::QsQuery as Query; use sqlx::SqliteConnection; use utoipa::OpenApi; -use crate::{ - custom_error::{AppError, CustomErrorCode}, - public_system::model::{ListSlice, Pagination}, - user_system::models::{ - user_configure::UserConfigure, - user_info::{GetUsersQuery, UserInfo, UserType}, - }, -}; +use crate::custom_error::{AppError, CustomErrorCode}; use super::{ - models::authenticated_user::{AuthenticatedUser, GetTokenQuery}, + model::authenticated_user::{AuthenticatedUser, GetTokenQuery}, AppState, }; diff --git a/server/src/services/ws_service.rs b/server/crates/elerp_service/src/ws_service.rs similarity index 96% rename from server/src/services/ws_service.rs rename to server/crates/elerp_service/src/ws_service.rs index ac08991..858f830 100644 --- a/server/src/services/ws_service.rs +++ b/server/crates/elerp_service/src/ws_service.rs @@ -10,15 +10,13 @@ use axum::{ Router, }; +use elerp_common::model::WebSocketFlags; use futures::{SinkExt, StreamExt}; use tracing::{error, info, warn}; -use crate::{ - public_system::model::WebSocketFlags, - services::models::web_socket_flag_json::WebSocketFlagsJson, -}; +use crate::model::web_socket_flag_json::WebSocketFlagsJson; -use super::{models::authenticated_user::AuthenticatedUser, AppState}; +use super::{model::authenticated_user::AuthenticatedUser, AppState}; pub fn get_services() -> Router { Router::new() @@ -148,7 +146,7 @@ async fn handle_socket( } else { continue; //Don't notice other users. } - }, + } WebSocketFlags::Ping => print_info = false, _ => (), } diff --git a/server/crates/guest_order_module/Cargo.toml b/server/crates/guest_order_module/Cargo.toml new file mode 100644 index 0000000..b415b19 --- /dev/null +++ b/server/crates/guest_order_module/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "guest_order_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +user_system = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } diff --git a/server/src/erp/guest_order_module.rs b/server/crates/guest_order_module/src/lib.rs similarity index 65% rename from server/src/erp/guest_order_module.rs rename to server/crates/guest_order_module/src/lib.rs index 9f66b80..e09d924 100644 --- a/server/src/erp/guest_order_module.rs +++ b/server/crates/guest_order_module/src/lib.rs @@ -1,37 +1,30 @@ -use std::sync::Arc; - use anyhow::bail; +use anyhow::Result; +use elerp_common::guest_order_module::model::guest_order::GetGuestOrdersQuery; +use elerp_common::guest_order_module::model::guest_order::GuestOrder; +use elerp_common::guest_order_module::model::guest_order::GuestOrderConfirm; +use elerp_common::guest_order_module::model::guest_order::GuestOrderStatus; +use elerp_common::model::action_type::ActionType; +use elerp_common::model::Pagination; +use elerp_common::model::WebSocketFlags; +use elerp_common::order_module; +use elerp_common::order_module::model::order::Order; +use elerp_common::order_module::model::order::OrderCurrency; +use elerp_common::sql; +use elerp_common::sql::is_exists_in_table; +use elerp_common::sql::remove_row_from_table; +use elerp_common::user_system::model::user_info::UserInfo; +use elerp_common::user_system::model::user_info::UserType; use futures::TryStreamExt; +use public_system::PublicSystem; use sqlx::{sqlite::SqliteRow, Row, SqliteConnection}; -use tokio::sync::RwLock; - -use crate::{ - public_system::{ - model::{Pagination, WebSocketFlags}, - PublicSystem, - }, - user_system::models::user_info::{UserInfo, UserType}, -}; - -use self::model::{GetGuestOrdersQuery, GuestOrder, GuestOrderStatus}; -use super::{ - dependency::ModuleDependency, - order_module::model::{Order, OrderCurrency}, - ActionType, Result, -}; - -pub mod model; #[derive(Debug, Clone)] pub struct GuestOrderModule { ps: PublicSystem, - dependency: Arc>>, } impl GuestOrderModule { - pub async fn set_dependency(&self, dep: ModuleDependency) { - *self.dependency.write().await = Some(dep); - } pub async fn new(ps: PublicSystem) -> Self { let mut tx = ps.begin_tx(true).await.unwrap(); sqlx::query( @@ -78,97 +71,67 @@ impl GuestOrderModule { .await .unwrap(); - let s = Self { - ps: ps.clone(), - dependency: Arc::new(RwLock::new(None)), - }; + let s = Self { ps: ps.clone() }; tx.commit().await.unwrap(); s } pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps - .is_exists_in_table("guest_orders", "id", id, tx) - .await + is_exists_in_table("guest_orders", "id", id, tx).await } - pub async fn is_token_match( - &self, - id: i64, - token: &str, - tx: &mut SqliteConnection, - ) -> Result { - Ok( - sqlx::query("SELECT id FROM guest_orders WHERE id=? AND sub_token=? LIMIT 1") - .bind(id) - .bind(token) - .fetch(&mut *tx) - .try_next() - .await? - .is_some(), - ) + pub async fn get_status(&self, id: i64, tx: &mut SqliteConnection) -> Result { + Ok(sqlx::query("SELECT guest_order_status FROM guest_orders WHERE id = ? LIMIT 1") + .bind(id) + .fetch_one(&mut *tx) + .await? + .get("guest_order_status")) + } + + pub async fn is_token_match(&self, id: i64, token: &str, tx: &mut SqliteConnection) -> Result { + Ok(sqlx::query("SELECT id FROM guest_orders WHERE id=? AND sub_token=? LIMIT 1") + .bind(id) + .bind(token) + .fetch(&mut *tx) + .try_next() + .await? + .is_some()) } pub fn preprocess(&self, order: &mut GuestOrder, user: &UserInfo, person_in_charge_id: i64) { order.created_by_user_id = user.id; order.person_in_charge_id = person_in_charge_id; + order.items = None; } pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { - let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM guest_orders;") - .fetch_one(&mut *tx) - .await? - .get("count"); + let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM guest_orders;").fetch_one(&mut *tx).await?.get("count"); Ok(count >= self.ps.get_config().limit.guest_orders) } - pub async fn confirm(&self, id: i64, order: &Order, tx: &mut SqliteConnection) -> Result<()> { - let now = self.ps.get_timestamp_seconds() as i64; - sqlx::query("UPDATE guest_orders SET order_id=?, description=?, guest_order_status=?, confirmed_date=? WHERE id=?") - .bind(order.id) - .bind(&order.description) - .bind(GuestOrderStatus::Confirmed) - .bind(now) - .bind(id) - .execute(&mut *tx) - .await?; - self.ps - .notice(WebSocketFlags::ConfirmGuestOrder(id)) - .await?; - Ok(()) + pub async fn can_access(&self, id: i64, user: &UserInfo, tx: &mut SqliteConnection) -> Result { + Ok(user.user_type == UserType::Admin + || sqlx::query("SELECT id FROM guest_orders WHERE id=? AND created_by_user_id=? LIMIT 1") + .bind(id) + .bind(user.id) + .fetch(&mut *tx) + .try_next() + .await? + .is_some()) } - pub async fn can_access( - &self, - id: i64, - user: &UserInfo, - tx: &mut SqliteConnection, - ) -> Result { - Ok(user.user_type == UserType::Admin - || sqlx::query( - "SELECT id FROM guest_orders WHERE id=? AND created_by_user_id=? LIMIT 1", - ) + pub async fn is_sub_token_same(&self, id: i64, sub_token: &str, tx: &mut SqliteConnection) -> Result { + Ok(sqlx::query("SELECT 1 FROM guest_orders WHERE id = ? AND sub_token = ? LIMIT 1") .bind(id) - .bind(user.id) - .fetch(&mut *tx) - .try_next() + .bind(sub_token) + .fetch_optional(&mut *tx) .await? .is_some()) } - pub async fn add( - &self, - sub_token: &str, - mut order: GuestOrder, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn add(&self, sub_token: &str, mut order: GuestOrder, tx: &mut SqliteConnection) -> Result { let now = self.ps.get_timestamp_seconds() as i64; - let order_category_id = if order.order_category_id < 10001 { - 10001 - } else { - order.order_category_id - }; let r = sqlx::query("INSERT INTO guest_orders (date, confirmed_date, sub_token, created_by_user_id, warehouse_id, currency, person_related_id, person_in_charge_id, description, order_type, guest_order_status, order_category_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") .bind(now) .bind(now) @@ -181,30 +144,24 @@ impl GuestOrderModule { .bind(&order.description) .bind(&order.order_type) .bind(GuestOrderStatus::Pending) - .bind(order_category_id) + .bind(order.order_category_id) .execute(&mut *tx) .await?; if r.rows_affected() != 1 { bail!("Can't insert the order to guest_orders!"); } + order.date = now; + order.confirmed_date = now; order.sub_token = sub_token.to_owned(); - order.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "guest_orders", tx) - .await?; + order.guest_order_status = GuestOrderStatus::Pending; + order.id = sql::try_set_standard_id(r.last_insert_rowid(), "guest_orders", tx).await?; - self.ps - .notice(WebSocketFlags::AddGuestOrder(order.id)) - .await?; + self.ps.notice(WebSocketFlags::AddGuestOrder(order.id)).await?; Ok(order) } pub async fn remove(&self, id: i64, notice: bool, tx: &mut SqliteConnection) -> Result { - if self - .ps - .remove_row_from_table(id, "guest_orders", tx) - .await? - { + if remove_row_from_table(id, "guest_orders", tx).await? { if notice { self.ps.notice(WebSocketFlags::RemoveGuestOrder(id)).await?; } @@ -213,6 +170,38 @@ impl GuestOrderModule { Ok(false) } + pub async fn confirm(&self, id: i64, v: GuestOrder, tx: &mut SqliteConnection) -> Result> { + if let Some(mut guest_order) = self.get(id, &mut *tx).await? { + guest_order.description = v.description; + guest_order.items = v.items; + + let order: Order = guest_order.into(); + + let mut result = GuestOrderConfirm { + check_result: order_module::check(&order, false, &mut *tx).await?, + order: None, + }; + if result.check_result.items_not_available.is_empty() { + let order = order_module::add(order, &mut *tx).await?; + let now = self.ps.get_timestamp_seconds() as i64; + sqlx::query("UPDATE guest_orders SET order_id=?, guest_order_status=?, confirmed_date=? WHERE id=?") + .bind(order.id) + .bind(GuestOrderStatus::Confirmed) + .bind(now) + .bind(id) + .execute(&mut *tx) + .await?; + let go = self.get(id, &mut *tx).await?.expect("Get empty guest order when confirm!"); + result.order = Some(go); + self.ps.notice(WebSocketFlags::ConfirmGuestOrder(id)).await?; + } + + Ok(Some(result)) + } else { + Ok(None) + } + } + async fn row_to_order(&self, row: SqliteRow, tx: &mut SqliteConnection) -> Result { let id = row.get("id"); let date = row.get("date"); @@ -276,31 +265,21 @@ impl GuestOrderModule { .fetch(&mut *tx) .try_next() .await?; - Ok(if let Some(row) = r { - Some(self.row_to_order(row, tx).await?) - } else { - None - }) + Ok(if let Some(row) = r { Some(self.row_to_order(row, tx).await?) } else { None }) } pub async fn get_order_id(&self, id: i64, tx: &mut SqliteConnection) -> Result> { - let r = sqlx::query("SELECT order_id FROM guest_orders WHERE id = ?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await?; - Ok(if let Some(row) = r { - Some(row.get("order_id")) - } else { - None - }) + let r = sqlx::query("SELECT order_id FROM guest_orders WHERE id = ?").bind(id).fetch(&mut *tx).try_next().await?; + Ok(if let Some(row) = r { Some(row.get("order_id")) } else { None }) } fn get_permission_inner(&self, action: ActionType) -> String { match action { ActionType::General(id) | ActionType::GeneralAllowed(id) => { - format!("INNER JOIN warehouse_permission - ON warehouse_permission.user_id={id} AND warehouse_permission.warehouse_id=guest_orders.warehouse_id") + format!( + "INNER JOIN warehouse_permission + ON warehouse_permission.user_id={id} AND warehouse_permission.warehouse_id=guest_orders.warehouse_id" + ) } ActionType::Admin | ActionType::System => String::new(), } @@ -330,13 +309,7 @@ impl GuestOrderModule { LEFT JOIN persons AS persons_in_charge ON guest_orders.person_in_charge_id=persons_in_charge.id INNER JOIN warehouses ON guest_orders.warehouse_id=warehouses.id"; - pub async fn get_multiple( - &self, - pagination: &Pagination, - query: &GetGuestOrdersQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple(&self, pagination: &Pagination, query: &GetGuestOrdersQuery, action: ActionType, tx: &mut SqliteConnection) -> Result> { let s = Self::SELECT_MULTIPLE; let qw = query.get_where_condition(); let ob = query.get_order_condition(); @@ -356,24 +329,16 @@ impl GuestOrderModule { Ok(arr) } - pub async fn get_multiple_ids( - &self, - pagination: &Pagination, - query: &GetGuestOrdersQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple_ids(&self, pagination: &Pagination, query: &GetGuestOrdersQuery, action: ActionType, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let inner = self.get_permission_inner(action); - let rows = sqlx::query(&format!( - "SELECT id FROM guest_orders {inner} {qw} LIMIT ? OFFSET ?" - )) - .bind(pagination.limit()) - .bind(pagination.offset()) - .fetch_all(&mut *tx) - .await?; + let rows = sqlx::query(&format!("SELECT id FROM guest_orders {inner} {qw} LIMIT ? OFFSET ?")) + .bind(pagination.limit()) + .bind(pagination.offset()) + .fetch_all(&mut *tx) + .await?; let mut arr = Vec::with_capacity(rows.len()); for row in rows { arr.push(row.get("id")) @@ -382,20 +347,11 @@ impl GuestOrderModule { Ok(arr) } - pub async fn get_count( - &self, - query: &GetGuestOrdersQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn get_count(&self, query: &GetGuestOrdersQuery, action: ActionType, tx: &mut SqliteConnection) -> Result { let s = Self::SELECT_MULTIPLE; let qw = query.get_where_condition(); let inner = self.get_permission_inner(action); - let row = sqlx::query(&format!( - "SELECT count(*) as count FROM ({s} {inner} {qw}) AS tbl" - )) - .fetch_one(&mut *tx) - .await?; + let row = sqlx::query(&format!("SELECT count(*) as count FROM ({s} {inner} {qw}) AS tbl")).fetch_one(&mut *tx).await?; Ok(row.get("count")) } } diff --git a/server/crates/inventory_module/Cargo.toml b/server/crates/inventory_module/Cargo.toml new file mode 100644 index 0000000..b672631 --- /dev/null +++ b/server/crates/inventory_module/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "inventory_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +public_system = { workspace = true } +elerp_common = { workspace = true } +order_module = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } +ahash ={ workspace = true } +futures ={ workspace = true } +tokio ={ workspace = true } +rust_xlsxwriter = { workspace = true } \ No newline at end of file diff --git a/server/crates/inventory_module/src/lib.rs b/server/crates/inventory_module/src/lib.rs new file mode 100644 index 0000000..854f740 --- /dev/null +++ b/server/crates/inventory_module/src/lib.rs @@ -0,0 +1,250 @@ +use std::path::PathBuf; + +use ahash::{HashMap, HashMapExt}; +use anyhow::Result; + +use elerp_common::{ + inventory_module::model::inventory::{GetInventoryQuery, InventoryProduct}, + model::{action_type::ActionType, Pagination}, +}; +use public_system::PublicSystem; +use sqlx::{FromRow, Row, SqliteConnection}; +use tokio::{fs, io::AsyncWriteExt}; + +#[derive(Debug, Clone)] +pub struct InventoryModule { + ps: PublicSystem, +} + +impl InventoryModule { + pub async fn create_table(&self, tx: &mut SqliteConnection) -> Result<()> { + sqlx::query( + "CREATE TABLE IF NOT EXISTS inventory( + warehouse_id INT NOT NULL, + sku_id INT NOT NULL, + sku_category_id INT NOT NULL, + quantity INT NOT NULL + )", + ) + .execute(&mut *tx) + .await + .unwrap(); + sqlx::query( + "CREATE INDEX IF NOT EXISTS inventory_warehouses + ON inventory(warehouse_id); + CREATE INDEX IF NOT EXISTS inventory_skus + ON inventory(sku_id); + CREATE INDEX IF NOT EXISTS inventory_warehouses_and_skus + ON inventory(warehouse_id, sku_id); + CREATE INDEX IF NOT EXISTS inventory_sku_categories + ON inventory(sku_category_id);", + ) + .execute(&mut *tx) + .await + .unwrap(); + + Ok(()) + } + + pub async fn new(ps: PublicSystem) -> Self { + let mut tx = ps.get_conn().begin().await.unwrap(); + let s = Self { ps }; + s.create_table(tx.as_mut()).await.unwrap(); + tx.commit().await.unwrap(); + s + } + + pub async fn get( + &self, + warehouse_id: i64, + sku_id: i64, + tx: &mut SqliteConnection, + ) -> Result> { + Ok(sqlx::query_as::<_, InventoryProduct>( + "SELECT * FROM inventory WHERE warehouse_id=? AND sku_id=? LIMIT 1", + ) + .bind(warehouse_id) + .bind(sku_id) + .fetch_optional(&mut *tx) + .await?) + } + + fn get_permission_inner(&self, action: ActionType) -> String { + match action { + ActionType::General(id) | ActionType::GeneralAllowed(id) => { + format!("INNER JOIN warehouse_permission ON warehouse_permission.user_id={id} AND warehouse_permission.warehouse_id=inventory.warehouse_id") + } + ActionType::Admin | ActionType::System => String::new(), + } + } + + const SELECT: &'static str = "SELECT + inventory.warehouse_id, + inventory.sku_id, + inventory.sku_category_id, + inventory.quantity, + + warehouses.name AS warehouse_name, + sku_list.name AS sku_name, + sku_categories.name AS sku_category_name + + FROM inventory + INNER JOIN warehouses ON inventory.warehouse_id=warehouses.id + INNER JOIN sku_list ON inventory.sku_id=sku_list.id + INNER JOIN sku_categories ON inventory.sku_category_id=sku_categories.id"; + + pub async fn list( + &self, + pagination: &Pagination, + query: &GetInventoryQuery, + action: ActionType, + tx: &mut SqliteConnection, + ) -> Result> { + let select = Self::SELECT; + let qw = query.get_where_condition(); + let ob = query.get_order_condition(); + let inner = self.get_permission_inner(action); + let rows = sqlx::query(&format!("{select} {inner} {qw} {ob} LIMIT ? OFFSET ?")) + .bind(pagination.limit()) + .bind(pagination.offset()) + .fetch_all(&mut *tx) + .await?; + let mut arr = Vec::with_capacity(rows.len()); + for row in rows { + arr.push(InventoryProduct::from_row(&row).unwrap()) + } + + Ok(arr) + } + + pub async fn get_excel( + &self, + query: &GetInventoryQuery, + action: ActionType, + tx: &mut SqliteConnection, + ) -> Result { + use rust_xlsxwriter::{Color, Format, FormatAlign, FormatBorder, Workbook, Worksheet}; + + let inner = self.get_permission_inner(action); + let qw = query.get_where_condition(); + let ob = query.get_order_condition(); + let rows = sqlx::query(&format!("SELECT * FROM inventory {inner} {qw} {ob}")) + .fetch_all(&mut *tx) + .await?; + let mut arr = Vec::with_capacity(rows.len()); + for row in rows { + arr.push(InventoryProduct::from_row(&row).unwrap()) + } + + // Create a new Excel file object. + let mut workbook = Workbook::new(); + // Create some formats to use in the worksheet. + let header_format = Format::new() + .set_background_color(Color::Theme(4, 0)) + .set_font_color(Color::Theme(0, 0)) + .set_border(FormatBorder::Thin) + .set_bold() + .set_align(FormatAlign::Center) + .set_align(FormatAlign::VerticalCenter); + let data_format = Format::new() + .set_border(FormatBorder::Thin) + .set_align(FormatAlign::Center) + .set_align(FormatAlign::VerticalCenter); + + // Add a worksheet to the workbook. + let worksheet: &mut Worksheet = workbook.add_worksheet(); + worksheet.write_row_with_format( + 0, + 0, + ["Warehouse", "SKU", "SKU Category", "Quantity"], + &header_format, + )?; + let mut warehouses: HashMap = HashMap::new(); + let mut skus: HashMap = HashMap::new(); + let mut sku_categories: HashMap = HashMap::new(); + for (i, p) in arr.iter().enumerate() { + let warehouse = if warehouses.contains_key(&p.warehouse_id) { + warehouses.get(&p.warehouse_id).unwrap() + } else { + let v = sqlx::query("SELECT name FROM warehouses WHERE id = ? LIMIT 1") + .bind(p.warehouse_id) + .fetch_one(&mut *tx) + .await? + .get("name"); + warehouses.insert(p.warehouse_id, v); + warehouses.get(&p.warehouse_id).unwrap() + }; + let sku = if skus.contains_key(&p.sku_id) { + skus.get(&p.sku_id).unwrap() + } else { + let v = sqlx::query("SELECT name FROM sku_list WHERE id = ? LIMIT 1") + .bind(p.sku_id) + .fetch_one(&mut *tx) + .await? + .get("name"); + skus.insert(p.sku_id, v); + skus.get(&p.sku_id).unwrap() + }; + let sku_category = if sku_categories.contains_key(&p.sku_category_id) { + sku_categories.get(&p.sku_category_id).unwrap() + } else { + let v = sqlx::query("SELECT name FROM sku_categories WHERE id = ? LIMIT 1") + .bind(p.sku_category_id) + .fetch_one(&mut *tx) + .await? + .get("name"); + sku_categories.insert(p.sku_category_id, v); + sku_categories.get(&p.sku_category_id).unwrap() + }; + + let row = (i + 1) as u32; + worksheet.write_row_with_format( + row, + 0, + [warehouse, sku, sku_category], + &data_format, + )?; + worksheet.write_with_format(row, 3, p.quantity, &data_format)?; + } + let excels = self.ps.get_data_path().join("excels").join("inventory"); + if !excels.is_dir() { + fs::create_dir_all(&excels).await?; + } + let path = excels.join(format!( + "inventory-{}.xlsx", + self.ps.get_timestamp_seconds() + )); + if path.is_file() { + fs::remove_file(&path).await?; + } + let mut file = fs::File::create(&path).await?; + let buffer = workbook.save_to_buffer()?; + file.write_all(&buffer).await?; + Ok(path) + } + + pub async fn clear_cache(&self) -> Result<()> { + let excels = self.ps.get_data_path().join("excels"); + if excels.is_dir() { + fs::remove_dir_all(&excels).await?; + } + Ok(()) + } + + pub async fn get_count( + &self, + query: &GetInventoryQuery, + action: ActionType, + tx: &mut SqliteConnection, + ) -> Result { + let s = Self::SELECT; + let qw = query.get_where_condition(); + let inner = self.get_permission_inner(action); + let row = sqlx::query(&format!( + "SELECT count(*) as count FROM ({s} {inner} {qw}) AS tbl" + )) + .fetch_one(&mut *tx) + .await?; + Ok(row.get("count")) + } +} diff --git a/server/crates/order_category_module/Cargo.toml b/server/crates/order_category_module/Cargo.toml new file mode 100644 index 0000000..b37da64 --- /dev/null +++ b/server/crates/order_category_module/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "order_category_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } +futures ={ workspace = true } \ No newline at end of file diff --git a/server/src/erp/order_category_module.rs b/server/crates/order_category_module/src/lib.rs similarity index 50% rename from server/src/erp/order_category_module.rs rename to server/crates/order_category_module/src/lib.rs index 8cc6b07..83ad2c1 100644 --- a/server/src/erp/order_category_module.rs +++ b/server/crates/order_category_module/src/lib.rs @@ -1,15 +1,14 @@ use anyhow::bail; -use futures::TryStreamExt; -use sqlx::{Row, SqliteConnection}; - -use crate::public_system::{ +use elerp_common::{ model::{Pagination, WebSocketFlags}, - PublicSystem, + order_category_module::model::order_category::{GetOrderCategoryQuery, OrderCategory}, + sql::{self, get_row_from_table, is_exists_in_table, remove_row_from_table, row_is_duplicate_col_in_table, rows_to_objects}, }; +use futures::TryStreamExt; +use sqlx::{Row, SqliteConnection}; -use self::model::{GetOrderCategoryQuery, OrderCategory}; -pub mod model; -use super::Result; +use anyhow::Result; +use public_system::PublicSystem; #[derive(Debug, Clone)] pub struct OrderCategoryModule { @@ -45,82 +44,47 @@ impl OrderCategoryModule { } pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps - .is_exists_in_table("order_categories", "id", id, tx) - .await + is_exists_in_table("order_categories", "id", id, tx).await } pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { - let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM order_categories;") - .fetch_one(&mut *tx) - .await? - .get("count"); + let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM order_categories;").fetch_one(&mut *tx).await?.get("count"); Ok(count >= self.ps.get_config().limit.order_categories) } - pub async fn is_exists_name( - &self, - name: &str, - prev: Option, - tx: &mut SqliteConnection, - ) -> Result { - self.ps - .row_is_duplicate_col_in_table(name, prev, "order_categories", "name", tx) - .await + pub async fn is_exists_name(&self, name: &str, prev: Option, tx: &mut SqliteConnection) -> Result { + row_is_duplicate_col_in_table(name, prev, "order_categories", "name", tx).await } - pub async fn add( - &self, - mut v: OrderCategory, - tx: &mut SqliteConnection, - ) -> Result { - let r = sqlx::query( - "INSERT INTO order_categories (name, description, color, text_color) VALUES(?, ?, ?, ?)", - ) - .bind(&v.name) - .bind(&v.description) - .bind(&v.color) - .bind(&v.text_color) - .execute(&mut *tx) - .await?; + pub async fn add(&self, mut v: OrderCategory, tx: &mut SqliteConnection) -> Result { + let r = sqlx::query("INSERT INTO order_categories (name, description, color, text_color) VALUES(?, ?, ?, ?)") + .bind(&v.name) + .bind(&v.description) + .bind(&v.color) + .bind(&v.text_color) + .execute(&mut *tx) + .await?; if r.rows_affected() != 1 { bail!("Can't add order category"); } - v.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "order_categories", tx) - .await?; - self.ps - .notice(WebSocketFlags::AddOrderCategory(v.id)) - .await?; + v.id = sql::try_set_standard_id(r.last_insert_rowid(), "order_categories", tx).await?; + self.ps.notice(WebSocketFlags::AddOrderCategory(v.id)).await?; Ok(v) } pub async fn remove(&self, id: i64, notice: bool, tx: &mut SqliteConnection) -> Result { - let r = self - .ps - .remove_row_from_table(id, "order_categories", tx) - .await?; + let r = remove_row_from_table(id, "order_categories", tx).await?; if notice { - self.ps - .notice(WebSocketFlags::RemoveOrderCategory(id)) - .await?; + self.ps.notice(WebSocketFlags::RemoveOrderCategory(id)).await?; } Ok(r) } pub async fn get(&self, id: i64, tx: &mut SqliteConnection) -> Result> { - self.ps - .get_row_from_table("order_categories", "id", id, tx) - .await + get_row_from_table("order_categories", "id", id, tx).await } - pub async fn get_multiple( - &self, - pagination: &Pagination, - query: &GetOrderCategoryQuery, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple(&self, pagination: &Pagination, query: &GetOrderCategoryQuery, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let ob = query.get_order_condition(); let rows = sqlx::query(&format!( @@ -137,15 +101,10 @@ impl OrderCategoryModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects(rows) + rows_to_objects(rows) } - pub async fn get_multiple_ids( - &self, - pagination: &Pagination, - query: &GetOrderCategoryQuery, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple_ids(&self, pagination: &Pagination, query: &GetOrderCategoryQuery, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let rows = sqlx::query(&format!( "SELECT @@ -160,41 +119,24 @@ impl OrderCategoryModule { Ok(rows.into_iter().map(|row| row.get("id")).collect()) } - pub async fn get_count( - &self, - query: &GetOrderCategoryQuery, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn get_count(&self, query: &GetOrderCategoryQuery, tx: &mut SqliteConnection) -> Result { let qw = query.get_where_condition(); - let row = sqlx::query(&format!( - "SELECT count(*) as count FROM order_categories {qw}" - )) - .fetch_one(&mut *tx) - .await?; + let row = sqlx::query(&format!("SELECT count(*) as count FROM order_categories {qw}")).fetch_one(&mut *tx).await?; Ok(row.get("count")) } - pub async fn update( - &self, - id: i64, - mut v: OrderCategory, - tx: &mut SqliteConnection, - ) -> Result> { - let r = sqlx::query( - "UPDATE order_categories SET name=?, description=?, color=?, text_color=? WHERE id=?", - ) - .bind(&v.name) - .bind(&v.description) - .bind(&v.color) - .bind(&v.text_color) - .bind(id) - .execute(&mut *tx) - .await?; + pub async fn update(&self, id: i64, mut v: OrderCategory, tx: &mut SqliteConnection) -> Result> { + let r = sqlx::query("UPDATE order_categories SET name=?, description=?, color=?, text_color=? WHERE id=?") + .bind(&v.name) + .bind(&v.description) + .bind(&v.color) + .bind(&v.text_color) + .bind(id) + .execute(&mut *tx) + .await?; Ok(if r.rows_affected() == 1 { v.id = id; - self.ps - .notice(WebSocketFlags::UpdateOrderCategory(v.id)) - .await?; + self.ps.notice(WebSocketFlags::UpdateOrderCategory(v.id)).await?; Some(v) } else { None @@ -202,11 +144,6 @@ impl OrderCategoryModule { } pub async fn is_depend_by_another(&self, id: i64, tx: &mut SqliteConnection) -> Result { - Ok(sqlx::query("SELECT id FROM orders WHERE order_status_id=?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some()) + Ok(sqlx::query("SELECT id FROM orders WHERE order_status_id=?").bind(id).fetch(&mut *tx).try_next().await?.is_some()) } } diff --git a/server/crates/order_module/Cargo.toml b/server/crates/order_module/Cargo.toml new file mode 100644 index 0000000..d31db7c --- /dev/null +++ b/server/crates/order_module/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "order_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +user_system = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } +ahash ={ workspace = true } +strum ={ workspace = true } +tokio ={ workspace = true } +futures ={ workspace = true } \ No newline at end of file diff --git a/server/crates/order_module/src/lib.rs b/server/crates/order_module/src/lib.rs new file mode 100644 index 0000000..4fe8d66 --- /dev/null +++ b/server/crates/order_module/src/lib.rs @@ -0,0 +1,512 @@ +use ahash::{HashMap, HashMapExt, HashSet, HashSetExt}; +use anyhow::Result; +use elerp_common::inventory_module; +use elerp_common::model::action_type::ActionType; +use elerp_common::model::Pagination; +use elerp_common::model::WebSocketFlags; +use elerp_common::order_module; +use elerp_common::order_module::model::order::GetOrdersQuery; +use elerp_common::order_module::model::order::Order; +use elerp_common::order_module::model::order::OrderCurrency; +use elerp_common::order_module::model::order::OrderItem; +use elerp_common::order_module::model::order::OrderPaymentStatus; +use elerp_common::order_module::model::order::OrderType; +use elerp_common::set_to_string; +use elerp_common::sql::{is_exists_in_table, remove_row_from_table, rows_to_objects}; +use elerp_common::user_system::model::user_info::UserInfo; +use elerp_common::user_system::model::user_info::UserType; +use futures::TryStreamExt; +use sqlx::{sqlite::SqliteRow, Row, SqliteConnection}; + +use public_system::PublicSystem; + +#[derive(Debug, Clone)] +pub struct OrderModule { + ps: PublicSystem, +} + +impl OrderModule { + pub async fn new(ps: PublicSystem) -> Self { + let mut tx = ps.begin_tx(true).await.unwrap(); + + sqlx::query( + "CREATE TABLE IF NOT EXISTS orders( + id INTEGER PRIMARY KEY, + from_guest_order_id INT NOT NULL, + created_by_user_id INT NOT NULL, + updated_by_user_id INT NOT NULL, + warehouse_id INT NOT NULL, + total_amount REAL NOT NULL, + total_amount_settled REAL NOT NULL, + order_payment_status TEXT NOT NULL, + currency TEXT NOT NULL, + person_related_id INT NOT NULL, + person_in_charge_id INT NOT NULL, + date INT NOT NULL, + last_updated_date INT NOT NULL, + description TEXT NOT NULL, + order_type TEXT NOT NULL, + order_category_id INT NOT NULL + )", + ) + .execute(tx.as_mut()) + .await + .unwrap(); + + sqlx::query( + "CREATE INDEX IF NOT EXISTS order_warehouses + ON orders(warehouse_id); + CREATE INDEX IF NOT EXISTS order_currencies + ON orders(currency); + CREATE INDEX IF NOT EXISTS order_person_related_ids + ON orders(person_related_id); + CREATE INDEX IF NOT EXISTS order_person_in_charge_ids + ON orders(person_in_charge_id); + CREATE INDEX IF NOT EXISTS order_order_types + ON orders(order_type); + CREATE INDEX IF NOT EXISTS order_order_category_ids + ON orders(order_category_id);", + ) + .execute(tx.as_mut()) + .await + .unwrap(); + + sqlx::query( + "CREATE TABLE IF NOT EXISTS order_items( + order_id INT NOT NULL, + sku_id INT NOT NULL, + sku_category_id INT NOT NULL, + quantity INT NOT NULL, + price REAL NOT NULL, + amount REAL NOT NULL, + exchanged BOOLEAN NOT NULL + )", + ) + .execute(tx.as_mut()) + .await + .unwrap(); + sqlx::query( + "CREATE INDEX IF NOT EXISTS order_items_order_ids + ON order_items(order_id); + CREATE INDEX IF NOT EXISTS order_items_skus + ON order_items(sku_id); + CREATE INDEX IF NOT EXISTS order_items_sku_categories + ON order_items(sku_category_id); + CREATE INDEX IF NOT EXISTS order_items_exchanged + ON order_items(exchanged);", + ) + .execute(tx.as_mut()) + .await + .unwrap(); + + let s = Self { ps: ps.clone() }; + + tx.commit().await.unwrap(); + s + } + + pub async fn get_order_payment_status(&self, id: i64, tx: &mut SqliteConnection) -> Result> { + Ok(sqlx::query("SELECT order_payment_status FROM orders WHERE id = ?") + .bind(id) + .fetch(&mut *tx) + .try_next() + .await? + .map(|row| row.get("order_payment_status"))) + } + + pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { + is_exists_in_table("orders", "id", id, tx).await + } + + pub async fn is_from_guest_order(&self, id: i64, tx: &mut SqliteConnection) -> Result { + if let Some(row) = sqlx::query("SELECT from_guest_order_id FROM orders WHERE id=?").bind(id).fetch(&mut *tx).try_next().await? { + let guest_id: i64 = row.get("from_guest_order_id"); + Ok(guest_id > 0) + } else { + Ok(false) + } + } + + pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { + let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM orders;").fetch_one(&mut *tx).await?.get("count"); + Ok(count >= self.ps.get_config().limit.orders) + } + + pub fn preprocess(&self, order: &mut Order, user: &UserInfo, initial: bool, person_in_charge_id: i64) { + if let Some(items) = order.items.clone() { + order.items = Some( + items + .into_iter() + .filter(|item| { + let pass_quantity = match order.order_type { + OrderType::StockIn | OrderType::StockOut | OrderType::Return => item.quantity > 0, + _ => true, + }; + let pass_exchange = if order.order_type != OrderType::Exchange { !item.exchanged } else { true }; + let pass_sku_not_empty = item.sku_id > 0; + + pass_quantity && pass_exchange && pass_sku_not_empty + }) + .collect(), + ); + }; + + let now = self.ps.get_timestamp_seconds() as i64; + order.updated_by_user_id = user.id; + if initial { + order.date = now; + order.created_by_user_id = user.id; + } + order.last_updated_date = now; + order.person_in_charge_id = person_in_charge_id; + order.from_guest_order_id = 0; + } + + pub async fn can_access(&self, id: i64, user: &UserInfo, tx: &mut SqliteConnection) -> Result { + Ok(user.user_type == UserType::Admin + || sqlx::query("SELECT id FROM orders WHERE id=? AND created_by_user_id=? LIMIT 1") + .bind(id) + .bind(user.id) + .fetch(&mut *tx) + .try_next() + .await? + .is_some()) + } + + pub async fn add(&self, order: Order, tx: &mut SqliteConnection) -> Result { + let order = order_module::add(order, tx).await?; + self.ps.notice(WebSocketFlags::AddOrder(order.id)).await?; + Ok(order) + } + + async fn exists_order_type(&self, ot: OrderType, order_start: &Order, tx: &mut SqliteConnection) -> Result { + Ok(sqlx::query("SELECT id FROM orders WHERE order_type=? AND date>=? AND id<>? LIMIT 1") + .bind(ot) + .bind(order_start.date) + .bind(order_start.id) + .fetch(&mut *tx) + .try_next() + .await? + .is_some()) + } + + pub async fn recall(&self, order: Order, action: ActionType, tx: &mut SqliteConnection) -> Result { + if !self.exists_order_type(OrderType::Calibration, &order, tx).await? { + let warehouse_id = order.warehouse_id; + let mut items = self.get_order_items(order.id, &Pagination::max(), tx).await?; + match &order.order_type { + OrderType::Return | OrderType::StockIn => { + inventory_module::change(warehouse_id, &items, OrderType::StockOut, tx).await?; + } + OrderType::StockOut => { + inventory_module::change(warehouse_id, &items, OrderType::StockIn, tx).await?; + } + OrderType::Exchange => { + for item in items.iter_mut() { + item.exchanged = !item.exchanged; + } + inventory_module::change(warehouse_id, &items, OrderType::Exchange, tx).await?; + } + OrderType::Calibration => { + let mut skus = HashSet::with_capacity(items.len()); + for item in items { + skus.insert(item.sku_id); + } + + self.recalc_all(Some(HashSet::from_iter([order.warehouse_id])), Some(skus), Some(&order), action, tx).await?; + } + OrderType::CalibrationStrict => { + self.recalc_all(Some(HashSet::from_iter([order.warehouse_id])), None, Some(&order), action, tx).await?; + } + OrderType::Verification | OrderType::VerificationStrict => (), + } + } + Ok(true) + } + + pub async fn recalc_all(&self, warehouse_ids: Option>, skus_filter: Option>, to_remove: Option<&Order>, action: ActionType, tx: &mut SqliteConnection) -> Result<()> { + match skus_filter { + Some(skus) => { + let skus = set_to_string(&skus, ","); + match &warehouse_ids { + Some(ids) => { + let ids = set_to_string(&ids, ","); + sqlx::query(&format!("UPDATE inventory SET quantity=0 WHERE warehouse_id IN ({ids}) AND sku_id IN ({skus})")) + .execute(&mut *tx) + .await?; + } + None => { + sqlx::query("UPDATE inventory SET quantity=0").execute(&mut *tx).await?; + } + } + } + None => match &warehouse_ids { + Some(ids) => { + let ids = set_to_string(&ids, ","); + sqlx::query(&format!("UPDATE inventory SET quantity=0 WHERE warehouse_id IN ({ids})")).execute(&mut *tx).await?; + } + None => { + sqlx::query("UPDATE inventory SET quantity=0").execute(&mut *tx).await?; + } + }, + } + + let warehouse_count = if warehouse_ids.is_none() { + sqlx::query("SELECT COUNT(1) AS count FROM warehouses") + .fetch_optional(&mut *tx) + .await? + .map(|row| row.get("count")) + .unwrap_or(0) + } else { + warehouse_ids.as_ref().unwrap().len() as i64 + }; + + let mut q = GetOrdersQuery::empty(); + q.sorters = Some(vec!["date".to_owned()]); + q.warehouse_ids = warehouse_ids; + + let order_total = self.get_count(&q, action, tx).await?; + + let mut temp = HashMap::>::with_capacity(warehouse_count as _); + let mut p = Pagination::new(-1, 100); // start from -1 because p.next() will return the next offset. + while p.offset() < order_total { + let mut orders = self.get_multiple(p.next(), &q, ActionType::System, tx).await?; + for order in orders.iter_mut() { + if let Some(to_remove) = to_remove { + if to_remove.id == order.id { + continue; + } + } + let items = self.get_order_items(order.id, &Pagination::max(), tx).await?; + for item in &items { + let it = temp.entry(order.warehouse_id).or_insert(HashMap::with_capacity(items.len())); + let qty = it.entry(item.sku_id).or_insert(0); + + *qty = inventory_module::calc_quantity_by_order_type(*qty, item, order.order_type); + } + } + } + for (warehouse_id, items) in temp { + let calibration_items: Vec = items + .into_iter() + .map(|(sku_id, quantity)| OrderItem { + sku_id, + quantity, + price: 0.0, + exchanged: false, + }) + .collect(); + inventory_module::change(warehouse_id, &calibration_items, OrderType::Calibration, &mut *tx).await?; + } + self.ps.notice(WebSocketFlags::RecalcOrders).await?; + Ok(()) + } + + pub async fn remove(&self, id: i64, recall: bool, notice: bool, action: ActionType, tx: &mut SqliteConnection) -> Result { + if let Some(order) = self.get(id, tx).await? { + let recalled = if recall { self.recall(order, action, tx).await? } else { true }; + if recalled { + remove_row_from_table(id, "orders", tx).await?; + let r = sqlx::query("DELETE FROM order_items WHERE order_id=?").bind(id).execute(&mut *tx).await?; + if notice { + self.ps.notice(WebSocketFlags::RemoveOrder(id)).await?; + } + return Ok(r.rows_affected() > 0); + } + } + Ok(false) + } + + fn row_to_order(&self, row: SqliteRow) -> Order { + let id = row.get("id"); + Order { + id, + from_guest_order_id: row.get("from_guest_order_id"), + created_by_user_id: row.get("created_by_user_id"), + updated_by_user_id: row.get("updated_by_user_id"), + currency: row.try_get("currency").unwrap_or(OrderCurrency::Unknown), + total_amount: row.get("total_amount"), + total_amount_settled: row.get("total_amount_settled"), + order_payment_status: row.get("order_payment_status"), + warehouse_id: row.get("warehouse_id"), + person_related_id: row.get("person_related_id"), + person_in_charge_id: row.get("person_in_charge_id"), + date: row.get("date"), + last_updated_date: row.get("last_updated_date"), + description: row.get("description"), + order_type: row.get("order_type"), + order_category_id: row.get("order_category_id"), + items: None, + } + } + + pub async fn get(&self, id: i64, tx: &mut SqliteConnection) -> Result> { + let r = sqlx::query("SELECT * FROM orders WHERE id = ?").bind(id).fetch(&mut *tx).try_next().await?; + Ok(if let Some(row) = r { Some(self.row_to_order(row)) } else { None }) + } + + pub async fn get_order_items(&self, order_id: i64, pagination: &Pagination, tx: &mut SqliteConnection) -> Result> { + let rows = sqlx::query("SELECT * FROM order_items WHERE order_id=? LIMIT ? OFFSET ?") + .bind(order_id) + .bind(pagination.limit()) + .bind(pagination.offset()) + .fetch_all(&mut *tx) + .await?; + let arr = rows_to_objects(rows)?; + Ok(arr) + } + + fn get_permission_inner(&self, action: ActionType) -> String { + match action { + ActionType::General(id) | ActionType::GeneralAllowed(id) => { + format!( + "INNER JOIN warehouse_permission + ON warehouse_permission.user_id={id} AND warehouse_permission.warehouse_id=orders.warehouse_id" + ) + } + ActionType::Admin | ActionType::System => String::new(), + } + } + const SELECT_MULTIPLE: &'static str = " + SELECT + orders.id, + orders.from_guest_order_id, + orders.created_by_user_id, + orders.updated_by_user_id, + orders.description, + orders.date, + orders.last_updated_date, + orders.person_related_id, + orders.person_in_charge_id, + orders.warehouse_id, + orders.currency, + orders.order_type, + orders.order_category_id, + orders.total_amount, + orders.total_amount_settled, + orders.order_payment_status, + persons_related.name AS person_related_name, + COALESCE(persons_in_charge.name, 'Empty') AS person_in_charge_name, + warehouses.name AS warehouse_name, + order_categories.name AS status_name + FROM orders + "; + const INNERS: &'static str = " + INNER JOIN persons AS persons_related ON orders.person_related_id=persons_related.id + LEFT JOIN persons AS persons_in_charge ON orders.person_in_charge_id=persons_in_charge.id + INNER JOIN order_categories ON orders.order_category_id=order_categories.id + INNER JOIN warehouses ON orders.warehouse_id=warehouses.id + "; + + pub async fn get_multiple(&self, pagination: &Pagination, query: &GetOrdersQuery, action: ActionType, tx: &mut SqliteConnection) -> Result> { + let s = Self::SELECT_MULTIPLE; + let inners = Self::INNERS; + let qw = query.get_where_condition(); + let ob = query.get_order_condition(); + + let inner = self.get_permission_inner(action); + + let rows = sqlx::query(&format!("{s} {inners} {inner} {qw} {ob} LIMIT ? OFFSET ?")) + .bind(pagination.limit()) + .bind(pagination.offset()) + .fetch_all(&mut *tx) + .await?; + let mut arr = Vec::with_capacity(rows.len()); + for row in rows { + arr.push(self.row_to_order(row)) + } + + Ok(arr) + } + + pub async fn get_multiple_ids(&self, pagination: &Pagination, query: &GetOrdersQuery, action: ActionType, tx: &mut SqliteConnection) -> Result> { + let qw = query.get_where_condition(); + + let inner = self.get_permission_inner(action); + + let rows = sqlx::query(&format!("SELECT id FROM orders {inner} {qw} LIMIT ? OFFSET ?")) + .bind(pagination.limit()) + .bind(pagination.offset()) + .fetch_all(&mut *tx) + .await?; + let mut arr = Vec::with_capacity(rows.len()); + for row in rows { + arr.push(row.get("id")) + } + + Ok(arr) + } + + pub async fn get_count(&self, query: &GetOrdersQuery, action: ActionType, tx: &mut SqliteConnection) -> Result { + let s = Self::SELECT_MULTIPLE; + let inners = Self::INNERS; + let qw = query.get_where_condition(); + let inner = self.get_permission_inner(action); + let row = sqlx::query(&format!("SELECT count(*) as count FROM ({s} {inners} {inner} {qw}) AS tbl")).fetch_one(&mut *tx).await?; + Ok(row.get("count")) + } + + pub async fn is_check_pass(&self, order: &Order, tx: &mut SqliteConnection) -> Result { + Ok(order_module::check(order, true, tx).await?.items_not_available.is_empty()) + } + + pub async fn update(&self, id: i64, mut v: Order, action: ActionType, tx: &mut SqliteConnection) -> Result> { + let r = + match action { + ActionType::GeneralAllowed(_) | ActionType::General(_) => { + sqlx::query("UPDATE orders SET updated_by_user_id=?, last_updated_date=?, person_related_id=?, person_in_charge_id=?, description=?, currency=?, order_category_id=? WHERE id=?") + .bind(v.updated_by_user_id) + .bind(v.last_updated_date) + .bind(v.person_related_id) + .bind(v.person_in_charge_id) + .bind(&v.description) + .bind(&v.currency) + .bind(v.order_category_id) + .bind(id) + .execute(&mut *tx) + .await? + } + ActionType::Admin => { + sqlx::query( + "UPDATE orders SET updated_by_user_id=?, last_updated_date=?, date=?, person_related_id=?, person_in_charge_id=?, description=?, currency=?, order_category_id=? WHERE id=?", + ) + .bind(v.updated_by_user_id) + .bind(v.last_updated_date) + .bind(v.date) + .bind(v.person_related_id) + .bind(v.person_in_charge_id) + .bind(&v.description) + .bind(&v.currency) + .bind(v.order_category_id) + .bind(id) + .execute(&mut *tx) + .await? + } + ActionType::System => sqlx::query( + "UPDATE orders SET date=?, person_related_id=?, person_in_charge_id=?, description=?, currency=?, order_category_id=?, total_amount_settled=?, order_payment_status=? WHERE id=?", + ) + .bind(v.date) + .bind(v.person_related_id) + .bind(v.person_in_charge_id) + .bind(&v.description) + .bind(&v.currency) + .bind(v.order_category_id) + .bind(v.total_amount_settled) + .bind(v.order_payment_status) + .bind(id) + .execute(&mut *tx) + .await?, + }; + Ok(if r.rows_affected() == 1 { + v.id = id; + self.ps.notice(WebSocketFlags::UpdateOrder(v.id)).await?; + Some(v) + } else { + None + }) + } + + pub async fn is_depend_by_another(&self, id: i64, tx: &mut SqliteConnection) -> Result { + Ok(sqlx::query("SELECT id FROM order_payments WHERE order_id=?").bind(id).fetch(&mut *tx).try_next().await?.is_some()) + } +} diff --git a/server/crates/order_payment_module/Cargo.toml b/server/crates/order_payment_module/Cargo.toml new file mode 100644 index 0000000..c9cfc51 --- /dev/null +++ b/server/crates/order_payment_module/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "order_payment_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } +futures ={ workspace = true } \ No newline at end of file diff --git a/server/src/erp/order_payment_module.rs b/server/crates/order_payment_module/src/lib.rs similarity index 58% rename from server/src/erp/order_payment_module.rs rename to server/crates/order_payment_module/src/lib.rs index 721a4a9..6e73bcf 100644 --- a/server/src/erp/order_payment_module.rs +++ b/server/crates/order_payment_module/src/lib.rs @@ -1,35 +1,25 @@ -use std::sync::Arc; - use anyhow::bail; +use anyhow::Context; +use anyhow::Result; +use elerp_common::model::action_type::ActionType; +use elerp_common::model::Pagination; +use elerp_common::model::WebSocketFlags; +use elerp_common::order_module::model::order::OrderPaymentStatus; +use elerp_common::order_payment_module::model::order_payment::GetOrderPaymentsQuery; +use elerp_common::order_payment_module::model::order_payment::OrderPayment; +use elerp_common::sql; +use elerp_common::sql::{get_row_from_table, is_exists_in_table, remove_row_from_table, rows_to_objects}; +use elerp_common::user_system::model::user_info::{UserInfo, UserType}; use futures::TryStreamExt; +use public_system::PublicSystem; use sqlx::{Row, SqliteConnection}; -use tokio::sync::RwLock; - -use crate::{ - public_system::{ - model::{Pagination, WebSocketFlags}, - PublicSystem, - }, - user_system::models::user_info::{UserInfo, UserType}, -}; - -use self::model::{GetOrderPaymentsQuery, OrderPayment}; -pub mod model; -use super::{ - dependency::ModuleDependency, order_module::model::OrderPaymentStatus, ActionType, Result, -}; #[derive(Debug, Clone)] pub struct OrderPaymentModule { ps: PublicSystem, - dependency: Arc>>, } impl OrderPaymentModule { - pub async fn set_dependency(&self, dep: ModuleDependency) { - *self.dependency.write().await = Some(dep); - } - pub async fn new(ps: PublicSystem) -> Self { let mut tx = ps.get_conn().begin().await.unwrap(); sqlx::query( @@ -58,25 +48,17 @@ impl OrderPaymentModule { .await .unwrap(); - let s = Self { - ps, - dependency: Arc::new(RwLock::new(None)), - }; + let s = Self { ps }; tx.commit().await.unwrap(); s } pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps - .is_exists_in_table("order_payments", "id", id, tx) - .await + is_exists_in_table("order_payments", "id", id, tx).await } pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { - let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM order_payments;") - .fetch_one(&mut *tx) - .await? - .get("count"); + let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM order_payments;").fetch_one(&mut *tx).await?.get("count"); Ok(count >= self.ps.get_config().limit.order_payments) } @@ -85,42 +67,38 @@ impl OrderPaymentModule { v.created_by_user_id = user.id; } - pub async fn can_access( - &self, - id: i64, - user: &UserInfo, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn can_access(&self, id: i64, user: &UserInfo, tx: &mut SqliteConnection) -> Result { Ok(user.user_type == UserType::Admin - || sqlx::query( - "SELECT id FROM order_payments WHERE id=? AND created_by_user_id=? LIMIT 1", - ) - .bind(id) - .bind(user.id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some()) + || sqlx::query("SELECT id FROM order_payments WHERE id=? AND created_by_user_id=? LIMIT 1") + .bind(id) + .bind(user.id) + .fetch(&mut *tx) + .try_next() + .await? + .is_some()) } - pub async fn add( - &self, - mut v: OrderPayment, - tx: &mut SqliteConnection, - ) -> Result { - let l = self.dependency.read().await; - let dep = l.as_ref().unwrap(); + pub async fn add(&self, mut v: OrderPayment, tx: &mut SqliteConnection) -> Result { Ok( - if let Some(mut order) = dep.order.get(v.order_id, tx).await? { - if order.order_payment_status == OrderPaymentStatus::Settled { + if let Some(order_row) = sqlx::query("SELECT order_payment_status, warehouse_id, total_amount, total_amount_settled FROM orders WHERE id=? LIMIT 1") + .bind(v.order_id) + .fetch_optional(&mut *tx) + .await? + { + let mut order_payment_status = order_row.get("order_payment_status"); + let warehouse_id: i64 = order_row.get("warehouse_id"); + let total_amount: f64 = order_row.get("total_amount"); + let mut total_amount_settled: f64 = order_row.get("total_amount_settled"); + + if order_payment_status == OrderPaymentStatus::Settled { bail!("Order's payment is settled!"); } let r = sqlx::query( - "INSERT INTO order_payments (order_id, warehouse_id, created_by_user_id, person_in_charge_id, total_amount, creation_date, actual_date, remark) VALUES(?, ?, ?, ?, ?, ?, ?, ?)", - ) - .bind(order.id) - .bind(order.warehouse_id) + "INSERT INTO order_payments (order_id, warehouse_id, created_by_user_id, person_in_charge_id, total_amount, creation_date, actual_date, remark) VALUES(?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind(v.order_id) + .bind(warehouse_id) .bind(v.created_by_user_id) .bind(v.person_in_charge_id) .bind(v.total_amount) @@ -133,24 +111,23 @@ impl OrderPaymentModule { bail!("Can't add order status"); } - order.total_amount_settled += v.total_amount; - order.order_payment_status = match order.total_amount_settled { - v if v >= order.total_amount => OrderPaymentStatus::Settled, + total_amount_settled += v.total_amount; + order_payment_status = match total_amount_settled { + v if v >= total_amount => OrderPaymentStatus::Settled, v if v > 0.0 => OrderPaymentStatus::PartialSettled, _ => OrderPaymentStatus::Unsettled, }; - dep.order - .update(order.id, order, ActionType::System, tx) - .await?; - - v.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "order_payments", tx) - .await?; - self.ps - .notice(WebSocketFlags::AddOrderPayment(v.id)) - .await?; - v + sqlx::query("UPDATE orders SET total_amount_settled = ?, order_payment_status = ? WHERE id = ?") + .bind(total_amount_settled) + .bind(order_payment_status) + .bind(v.order_id) + .execute(&mut *tx) + .await + .with_context(|| "update order's amount settled failed!")?; + + v.id = sql::try_set_standard_id(r.last_insert_rowid(), "order_payments", tx).await?; + self.ps.notice(WebSocketFlags::AddOrderPayment(v.id)).await?; + OrderPayment { warehouse_id, ..v } } else { bail!("Order is not found!"); }, @@ -159,10 +136,7 @@ impl OrderPaymentModule { pub async fn remove(&self, id: i64, notice: bool, tx: &mut SqliteConnection) -> Result { if let Some(op) = self.get(id, tx).await? { - let r = self - .ps - .remove_row_from_table(id, "order_payments", tx) - .await?; + let r = remove_row_from_table(id, "order_payments", tx).await?; if r { sqlx::query( "UPDATE orders SET total_amount_settled = total_amount_settled - ?, @@ -186,9 +160,7 @@ impl OrderPaymentModule { // }; } if notice { - self.ps - .notice(WebSocketFlags::RemoveOrderPayment(id)) - .await?; + self.ps.notice(WebSocketFlags::RemoveOrderPayment(id)).await?; } Ok(r) } else { @@ -197,28 +169,22 @@ impl OrderPaymentModule { } pub async fn get(&self, id: i64, tx: &mut SqliteConnection) -> Result> { - self.ps - .get_row_from_table("order_payments", "id", id, tx) - .await + get_row_from_table("order_payments", "id", id, tx).await } fn get_permission_inner(&self, action: ActionType) -> String { match action { ActionType::General(id) | ActionType::GeneralAllowed(id) => { - format!("INNER JOIN warehouse_permission - ON warehouse_permission.user_id={id} AND warehouse_permission.warehouse_id=order_payments.warehouse_id") + format!( + "INNER JOIN warehouse_permission + ON warehouse_permission.user_id={id} AND warehouse_permission.warehouse_id=order_payments.warehouse_id" + ) } ActionType::Admin | ActionType::System => String::new(), } } - pub async fn get_multiple( - &self, - pagination: &Pagination, - query: &GetOrderPaymentsQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple(&self, pagination: &Pagination, query: &GetOrderPaymentsQuery, action: ActionType, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let ob = query.get_order_condition(); let inner = self.get_permission_inner(action); @@ -241,16 +207,10 @@ impl OrderPaymentModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects(rows) + rows_to_objects(rows) } - pub async fn get_multiple_ids( - &self, - pagination: &Pagination, - query: &GetOrderPaymentsQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple_ids(&self, pagination: &Pagination, query: &GetOrderPaymentsQuery, action: ActionType, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let inner = self.get_permission_inner(action); let rows = sqlx::query(&format!( @@ -267,19 +227,10 @@ impl OrderPaymentModule { Ok(rows.into_iter().map(|row| row.get("id")).collect()) } - pub async fn get_count( - &self, - query: &GetOrderPaymentsQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn get_count(&self, query: &GetOrderPaymentsQuery, action: ActionType, tx: &mut SqliteConnection) -> Result { let qw = query.get_where_condition(); let inner = self.get_permission_inner(action); - let row = sqlx::query(&format!( - "SELECT count(*) as count FROM order_payments {inner} {qw}" - )) - .fetch_one(&mut *tx) - .await?; + let row = sqlx::query(&format!("SELECT count(*) as count FROM order_payments {inner} {qw}")).fetch_one(&mut *tx).await?; Ok(row.get("count")) } } diff --git a/server/crates/person_module/Cargo.toml b/server/crates/person_module/Cargo.toml new file mode 100644 index 0000000..c39530e --- /dev/null +++ b/server/crates/person_module/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "person_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } diff --git a/server/src/erp/person_module.rs b/server/crates/person_module/src/lib.rs similarity index 91% rename from server/src/erp/person_module.rs rename to server/crates/person_module/src/lib.rs index 3ecfad2..9b6ff1f 100644 --- a/server/src/erp/person_module.rs +++ b/server/crates/person_module/src/lib.rs @@ -1,16 +1,15 @@ -use anyhow::bail; +use anyhow::{bail, Result}; +use elerp_common::sql::{ + self, get_row_from_table, is_exists_in_table, remove_row_from_table, row_is_duplicate_col_in_table, rows_to_objects +}; +use elerp_common::{ + model::{action_type::ActionType, Pagination, WebSocketFlags}, + person_module::model::person::{GetPersonsQuery, Person}, +}; use futures::TryStreamExt; +use public_system::PublicSystem; use sqlx::{Row, SqliteConnection}; -use crate::public_system::{ - model::{Pagination, WebSocketFlags}, - PublicSystem, -}; - -use self::model::{GetPersonsQuery, Person}; -use super::{ActionType, Result}; -pub mod model; - #[derive(Debug, Clone)] pub struct PersonModule { ps: PublicSystem, @@ -52,7 +51,7 @@ impl PersonModule { } pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps.is_exists_in_table("persons", "id", id, tx).await + is_exists_in_table("persons", "id", id, tx).await } pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { @@ -69,9 +68,7 @@ impl PersonModule { prev: Option, tx: &mut SqliteConnection, ) -> Result { - self.ps - .row_is_duplicate_col_in_table(name, prev, "persons", "name", tx) - .await + row_is_duplicate_col_in_table(name, prev, "persons", "name", tx).await } pub async fn add(&self, mut person: Person, tx: &mut SqliteConnection) -> Result { @@ -93,16 +90,14 @@ impl PersonModule { bail!("Can't add person!"); } - person.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "persons", tx) + person.id = sql::try_set_standard_id(r.last_insert_rowid(), "persons", tx) .await?; self.ps.notice(WebSocketFlags::AddPerson(person.id)).await?; Ok(person) } pub async fn remove(&self, id: i64, notice: bool, tx: &mut SqliteConnection) -> Result { - let r = self.ps.remove_row_from_table(id, "persons", tx).await?; + let r = remove_row_from_table(id, "persons", tx).await?; if notice { self.ps.notice(WebSocketFlags::RemovePerson(id)).await?; } @@ -115,8 +110,7 @@ impl PersonModule { action: ActionType, tx: &mut SqliteConnection, ) -> Result> { - self.ps - .get_row_from_table("persons", "id", id, tx) + get_row_from_table("persons", "id", id, tx) .await .map(|opt: Option| { opt.map(|mut v| { @@ -168,7 +162,7 @@ impl PersonModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects(rows).map(|mut list: Vec| { + rows_to_objects(rows).map(|mut list: Vec| { for v in list.iter_mut() { match action { ActionType::General(_) => { diff --git a/server/crates/public_system/Cargo.toml b/server/crates/public_system/Cargo.toml new file mode 100644 index 0000000..d24f950 --- /dev/null +++ b/server/crates/public_system/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "public_system" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +ahash = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true } +utoipa = { workspace = true } +chrono = { workspace = true } +rand = { workspace = true } +strum = { workspace = true } +elerp_common = { workspace = true} \ No newline at end of file diff --git a/server/src/db.rs b/server/crates/public_system/src/db.rs similarity index 98% rename from server/src/db.rs rename to server/crates/public_system/src/db.rs index aa0f21f..f5415d5 100644 --- a/server/src/db.rs +++ b/server/crates/public_system/src/db.rs @@ -7,7 +7,7 @@ use sqlx::{ }; use tokio::fs; -use crate::config::AppConfig; +use elerp_common::config::AppConfig; pub async fn init_db(config: &AppConfig, one_time: bool) -> Result> { let path = config.data_path.join("elerp.db"); diff --git a/server/src/public_system.rs b/server/crates/public_system/src/lib.rs similarity index 61% rename from server/src/public_system.rs rename to server/crates/public_system/src/lib.rs index a326822..f812d07 100644 --- a/server/src/public_system.rs +++ b/server/crates/public_system/src/lib.rs @@ -1,5 +1,4 @@ use std::{ - borrow::Cow, collections::hash_map::DefaultHasher, hash::Hasher, ops::{Deref, DerefMut}, @@ -9,11 +8,10 @@ use std::{ }; use ahash::{HashMap, HashMapExt}; +use elerp_common::model::WebSocketFlags; use futures::TryStreamExt; -use sqlx::{ - sqlite::SqliteRow, Encode, FromRow, Pool, Row, Sqlite, SqliteConnection, Transaction, Type, -}; +use sqlx::{Pool, Row, Sqlite, SqliteConnection, Transaction}; use anyhow::Result; @@ -25,11 +23,9 @@ use tokio::{ }; use tracing::{error, info}; -use crate::config::AppConfig; - -use self::model::web_socket_flags::WebSocketFlags; +use elerp_common::config::AppConfig; -pub mod model; +pub mod db; #[derive(Debug, Clone)] pub struct PublicSystem { @@ -66,10 +62,13 @@ impl<'a> SqliteSafeTransaction<'a> { } } -const STANDARD_ID_NUM: i64 = 10000; - impl PublicSystem { - pub async fn new(pool: Pool, config: AppConfig) -> Self { + pub async fn update(config: AppConfig) -> bool { + let pool = db::init_db(&config, false).await.expect("Init db failed!"); + db::update(pool).await + } + pub async fn new(config: AppConfig) -> Self { + let pool = db::init_db(&config, false).await.expect("Init db failed!"); sqlx::query( "CREATE TABLE IF NOT EXISTS backup_records( filename TEXT NOT NULL, @@ -138,29 +137,6 @@ impl PublicSystem { } } - pub async fn try_set_standard_id( - &self, - creation_id: i64, - table_name: &str, - tx: &mut SqliteConnection, - ) -> Result { - if creation_id == 1 { - let nid = STANDARD_ID_NUM + 1; - sqlx::query(&Cow::Owned(format!( - "UPDATE {table_name} SET id = {nid} WHERE id = {creation_id}" - ))) - .execute(&mut *tx) - .await?; - Ok(nid) - } else { - Ok(creation_id as _) - } - } - - pub fn get_standard_id(&self, id: i64) -> i64 { - STANDARD_ID_NUM + id - } - pub fn get_data_path(&self) -> &PathBuf { &self.config.data_path } @@ -177,9 +153,7 @@ impl PublicSystem { pub async fn clear_cache(&self) -> Result<()> { let mut entrys = fs::read_dir(&self.config.data_path).await?; while let Some(entry) = entrys.next_entry().await? { - if entry.file_type().await?.is_file() - && entry.file_name().to_str().unwrap().starts_with("backup-") - { + if entry.file_type().await?.is_file() && entry.file_name().to_str().unwrap().starts_with("backup-") { fs::remove_file(entry.path()).await?; } } @@ -188,9 +162,7 @@ impl PublicSystem { } pub async fn get_backup_count(&self, tx: &mut SqliteConnection) -> Result { - let row = sqlx::query(&format!("SELECT count(*) AS count FROM backup_records")) - .fetch_one(&mut *tx) - .await?; + let row = sqlx::query(&format!("SELECT count(*) AS count FROM backup_records")).fetch_one(&mut *tx).await?; Ok(row.get("count")) } @@ -198,19 +170,13 @@ impl PublicSystem { let mut tx = self.begin_tx(true).await?; let count = self.get_backup_count(tx.as_mut()).await?; if count > 8 { - let r = sqlx::query("SELECT filename FROM backup_records ORDER BY date LIMIT 1") - .fetch(tx.as_mut()) - .try_next() - .await?; + let r = sqlx::query("SELECT filename FROM backup_records ORDER BY date LIMIT 1").fetch(tx.as_mut()).try_next().await?; if let Some(row) = r { let filename: String = row.get("filename"); let target = self.config.data_path.join(&filename); if target.is_file() { fs::remove_file(target).await?; - sqlx::query("DELETE FROM backup_records WHERE filename=?") - .bind(&filename) - .execute(tx.as_mut()) - .await?; + sqlx::query("DELETE FROM backup_records WHERE filename=?").bind(&filename).execute(tx.as_mut()).await?; } } } @@ -221,11 +187,7 @@ impl PublicSystem { self.process_limit_backup().await?; let now = self.get_timestamp_seconds() as i64; let filename = format!("backup-{now}.db"); - let r = sqlx::query("INSERT INTO backup_records VALUES (?, ?)") - .bind(&filename) - .bind(now) - .execute(&self.pool) - .await?; + let r = sqlx::query("INSERT INTO backup_records VALUES (?, ?)").bind(&filename).bind(now).execute(&self.pool).await?; if r.rows_affected() == 1 { let original = self.config.data_path.join("elerp.db"); @@ -242,95 +204,13 @@ impl PublicSystem { pub async fn begin_tx(&self, write: bool) -> Result> { let mut tx = self.pool.begin().await?; if write { - sqlx::query("UPDATE public_table SET reserved=1 WHERE id=1") - .execute(tx.as_mut()) - .await?; + sqlx::query("UPDATE public_table SET reserved=1 WHERE id=1").execute(tx.as_mut()).await?; } Ok(SqliteSafeTransaction::new(tx)) } - pub async fn row_is_duplicate_col_in_table( - &self, - col: &str, - prev: Option, - table_name: &str, - col_name: &str, - tx: &mut SqliteConnection, - ) -> Result { - let q1 = format!("SELECT id FROM {table_name} WHERE {col_name}=? AND id<>? LIMIT 1"); - let q2 = format!("SELECT id FROM {table_name} WHERE {col_name}=? LIMIT 1"); - let mut r = if let Some(prev) = prev { - sqlx::query(&q1).bind(col).bind(prev).fetch(&mut *tx) - } else { - sqlx::query(&q2).bind(col).fetch(&mut *tx) - }; - Ok(r.try_next().await?.is_some()) - } - - pub async fn get_row_from_table( - &self, - table_name: &str, - col_name: &str, - col_value: V, - tx: &mut SqliteConnection, - ) -> Result> - where - for<'q> V: 'q + Send + Encode<'q, Sqlite> + Type, - for<'r> T: FromRow<'r, SqliteRow> + Unpin + Send, - { - let q = format!("SELECT * FROM {table_name} WHERE {col_name} = ?"); - let mut r = sqlx::query(&q).bind(col_value).fetch(&mut *tx); - Ok(if let Some(row) = r.try_next().await? { - let v = T::from_row(&row)?; - Some(v) - } else { - None - }) - } - - pub async fn is_exists_in_table( - &self, - table_name: &str, - col_name: &str, - col_value: V, - tx: &mut SqliteConnection, - ) -> Result - where - for<'q> V: 'q + Send + Encode<'q, Sqlite> + Type, - { - let q = format!("SELECT {col_name} FROM {table_name} WHERE {col_name} = ?"); - let mut r = sqlx::query(&q).bind(col_value).fetch(&mut *tx); - Ok(r.try_next().await?.is_some()) - } - - pub fn rows_to_objects<'a, T>(&self, rows: Vec) -> Result> - where - for<'r> T: FromRow<'r, SqliteRow> + Unpin + Send + 'a, - { - let mut arr = Vec::with_capacity(rows.len()); - for row in rows { - arr.push(T::from_row(&row)?) - } - - Ok(arr) - } - - pub async fn remove_row_from_table( - &self, - row_id: i64, - table_name: &str, - tx: &mut SqliteConnection, - ) -> Result { - let q = format!("DELETE FROM {table_name} WHERE id = ?"); - let r = sqlx::query(&q).bind(row_id).execute(&mut *tx).await?; - Ok(r.rows_affected() == 1) - } - pub fn get_timestamp_seconds(&self) -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() } pub async fn exists_table(&self, table_name: &str, tx: &mut SqliteConnection) -> bool { @@ -339,12 +219,7 @@ impl PublicSystem { SELECT name FROM sqlite_master WHERE type='table' AND name=?; "#; - match sqlx::query(query) - .bind(table_name) - .fetch_optional(&mut *tx) - .await - .unwrap() - { + match sqlx::query(query).bind(table_name).fetch_optional(&mut *tx).await.unwrap() { Some(_row) => true, None => false, } @@ -354,16 +229,11 @@ impl PublicSystem { use chrono::*; let naive_time_max = NaiveTime::from_hms_milli_opt(23, 59, 59, 999).unwrap(); let local: DateTime = Local::now(); - let start = NaiveDate::from_ymd_opt(local.year(), local.month(), 1) - .unwrap() - .and_time(NaiveTime::MIN); + let start = NaiveDate::from_ymd_opt(local.year(), local.month(), 1).unwrap().and_time(NaiveTime::MIN); let end = NaiveDate::from_ymd_opt(local.year(), local.month(), 31) .unwrap_or( - NaiveDate::from_ymd_opt(local.year(), local.month(), 30).unwrap_or( - NaiveDate::from_ymd_opt(local.year(), local.month(), 29).unwrap_or( - NaiveDate::from_ymd_opt(local.year(), local.month(), 28).unwrap(), - ), - ), + NaiveDate::from_ymd_opt(local.year(), local.month(), 30) + .unwrap_or(NaiveDate::from_ymd_opt(local.year(), local.month(), 29).unwrap_or(NaiveDate::from_ymd_opt(local.year(), local.month(), 28).unwrap())), ) .and_time(naive_time_max); let sts = Local.from_local_datetime(&start).unwrap(); diff --git a/server/crates/sku_category_module/Cargo.toml b/server/crates/sku_category_module/Cargo.toml new file mode 100644 index 0000000..5478eec --- /dev/null +++ b/server/crates/sku_category_module/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sku_category_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +public_system = { workspace = true } +elerp_common = { workspace = true } +futures = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } diff --git a/server/src/erp/sku_category_module.rs b/server/crates/sku_category_module/src/lib.rs similarity index 51% rename from server/src/erp/sku_category_module.rs rename to server/crates/sku_category_module/src/lib.rs index 0381ec6..1f31a60 100644 --- a/server/src/erp/sku_category_module.rs +++ b/server/crates/sku_category_module/src/lib.rs @@ -1,15 +1,15 @@ use anyhow::bail; -use futures::TryStreamExt; -use sqlx::{Row, SqliteConnection}; - -use crate::public_system::{ +use elerp_common::sql::{get_row_from_table, is_exists_in_table, remove_row_from_table, row_is_duplicate_col_in_table, rows_to_objects}; +use elerp_common::{ model::{Pagination, WebSocketFlags}, - PublicSystem, + sku_category_module::model::sku_category::{GetSKUCategoriesQuery, SKUCategory}, + sql, }; +use futures::TryStreamExt; +use sqlx::{Row, SqliteConnection}; -use self::model::{GetSKUCategoriesQuery, SKUCategory}; -pub mod model; -use super::Result; +use anyhow::Result; +use public_system::PublicSystem; #[derive(Debug, Clone)] pub struct SKUCategoryModule { @@ -44,76 +44,47 @@ impl SKUCategoryModule { } pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps - .is_exists_in_table("sku_categories", "id", id, tx) - .await + is_exists_in_table("sku_categories", "id", id, tx).await } pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { - let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM sku_categories;") - .fetch_one(&mut *tx) - .await? - .get("count"); + let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM sku_categories;").fetch_one(&mut *tx).await?.get("count"); Ok(count >= self.ps.get_config().limit.sku_categories) } - pub async fn is_exists_name( - &self, - name: &str, - prev: Option, - tx: &mut SqliteConnection, - ) -> Result { - self.ps - .row_is_duplicate_col_in_table(name, prev, "sku_categories", "name", tx) - .await + pub async fn is_exists_name(&self, name: &str, prev: Option, tx: &mut SqliteConnection) -> Result { + row_is_duplicate_col_in_table(name, prev, "sku_categories", "name", tx).await } pub async fn add(&self, mut v: SKUCategory, tx: &mut SqliteConnection) -> Result { - let r = sqlx::query( - "INSERT INTO sku_categories (name, description, color, text_color) VALUES(?, ?, ?, ?)", - ) - .bind(&v.name) - .bind(&v.description) - .bind(&v.color) - .bind(&v.text_color) - .execute(&mut *tx) - .await?; + let r = sqlx::query("INSERT INTO sku_categories (name, description, color, text_color) VALUES(?, ?, ?, ?)") + .bind(&v.name) + .bind(&v.description) + .bind(&v.color) + .bind(&v.text_color) + .execute(&mut *tx) + .await?; if r.rows_affected() != 1 { bail!("Can't add sku category"); } - v.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "sku_categories", tx) - .await?; + v.id = sql::try_set_standard_id(r.last_insert_rowid(), "sku_categories", tx).await?; self.ps.notice(WebSocketFlags::AddSKUCategory(v.id)).await?; Ok(v) } pub async fn remove(&self, id: i64, notice: bool, tx: &mut SqliteConnection) -> Result { - let r = self - .ps - .remove_row_from_table(id, "sku_categories", tx) - .await?; + let r = remove_row_from_table(id, "sku_categories", tx).await?; if notice { - self.ps - .notice(WebSocketFlags::RemoveSKUCategory(id)) - .await?; + self.ps.notice(WebSocketFlags::RemoveSKUCategory(id)).await?; } Ok(r) } pub async fn get(&self, id: i64, tx: &mut SqliteConnection) -> Result> { - self.ps - .get_row_from_table("sku_categories", "id", id, tx) - .await + get_row_from_table("sku_categories", "id", id, tx).await } - pub async fn get_multiple( - &self, - pagination: &Pagination, - query: &GetSKUCategoriesQuery, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple(&self, pagination: &Pagination, query: &GetSKUCategoriesQuery, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let ob = query.get_order_condition(); let rows = sqlx::query(&format!( @@ -130,15 +101,10 @@ impl SKUCategoryModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects(rows) + rows_to_objects(rows) } - pub async fn get_multiple_ids( - &self, - pagination: &Pagination, - query: &GetSKUCategoriesQuery, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple_ids(&self, pagination: &Pagination, query: &GetSKUCategoriesQuery, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let rows = sqlx::query(&format!( "SELECT @@ -153,41 +119,24 @@ impl SKUCategoryModule { Ok(rows.into_iter().map(|row| row.get("id")).collect()) } - pub async fn get_count( - &self, - query: &GetSKUCategoriesQuery, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn get_count(&self, query: &GetSKUCategoriesQuery, tx: &mut SqliteConnection) -> Result { let qw = query.get_where_condition(); - let row = sqlx::query(&format!( - "SELECT count(*) as count FROM sku_categories {qw}" - )) - .fetch_one(&mut *tx) - .await?; + let row = sqlx::query(&format!("SELECT count(*) as count FROM sku_categories {qw}")).fetch_one(&mut *tx).await?; Ok(row.get("count")) } - pub async fn update( - &self, - id: i64, - mut v: SKUCategory, - tx: &mut SqliteConnection, - ) -> Result> { - let r = sqlx::query( - "UPDATE sku_categories SET name=?, description=?, color=?, text_color=? WHERE id=?", - ) - .bind(&v.name) - .bind(&v.description) - .bind(&v.color) - .bind(&v.text_color) - .bind(id) - .execute(&mut *tx) - .await?; + pub async fn update(&self, id: i64, mut v: SKUCategory, tx: &mut SqliteConnection) -> Result> { + let r = sqlx::query("UPDATE sku_categories SET name=?, description=?, color=?, text_color=? WHERE id=?") + .bind(&v.name) + .bind(&v.description) + .bind(&v.color) + .bind(&v.text_color) + .bind(id) + .execute(&mut *tx) + .await?; Ok(if r.rows_affected() == 1 { v.id = id; - self.ps - .notice(WebSocketFlags::UpdateSKUCategory(v.id)) - .await?; + self.ps.notice(WebSocketFlags::UpdateSKUCategory(v.id)).await?; Some(v) } else { None @@ -195,13 +144,6 @@ impl SKUCategoryModule { } pub async fn is_depend_by_another(&self, id: i64, tx: &mut SqliteConnection) -> Result { - Ok( - sqlx::query("SELECT id FROM sku_list WHERE sku_category_id=?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some(), - ) + Ok(sqlx::query("SELECT id FROM sku_list WHERE sku_category_id=?").bind(id).fetch(&mut *tx).try_next().await?.is_some()) } } diff --git a/server/crates/sku_module/Cargo.toml b/server/crates/sku_module/Cargo.toml new file mode 100644 index 0000000..3882e62 --- /dev/null +++ b/server/crates/sku_module/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sku_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +public_system = { workspace = true } +elerp_common = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } +futures ={ workspace = true } \ No newline at end of file diff --git a/server/src/erp/sku_module.rs b/server/crates/sku_module/src/lib.rs similarity index 67% rename from server/src/erp/sku_module.rs rename to server/crates/sku_module/src/lib.rs index b1f3af0..8a89f30 100644 --- a/server/src/erp/sku_module.rs +++ b/server/crates/sku_module/src/lib.rs @@ -1,15 +1,14 @@ use anyhow::bail; -use futures::TryStreamExt; -use sqlx::{Row, SqliteConnection}; - -use crate::public_system::{ +use elerp_common::sql::{self, get_row_from_table, is_exists_in_table, remove_row_from_table, rows_to_objects}; +use elerp_common::{ model::{Pagination, WebSocketFlags}, - PublicSystem, + sku_module::model::sku::{GetSKUsQuery, SKU}, }; -pub mod model; -use self::model::{GetSKUsQuery, SKU}; +use futures::TryStreamExt; +use sqlx::{Row, SqliteConnection}; -use super::Result; +use anyhow::Result; +use public_system::PublicSystem; #[derive(Debug, Clone)] pub struct SKUModule { @@ -48,32 +47,19 @@ impl SKUModule { #[allow(dead_code)] pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps.is_exists_in_table("sku_list", "id", id, tx).await + is_exists_in_table("sku_list", "id", id, tx).await } pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { - let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM sku_list;") - .fetch_one(&mut *tx) - .await? - .get("count"); + let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM sku_list;").fetch_one(&mut *tx).await?.get("count"); Ok(count >= self.ps.get_config().limit.skus) } - pub async fn is_exists_name( - &self, - name: &str, - category: i64, - prev: Option, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn is_exists_name(&self, name: &str, category: i64, prev: Option, tx: &mut SqliteConnection) -> Result { let q1 = "SELECT id FROM sku_list WHERE name=? AND sku_category_id=? AND id<>? LIMIT 1"; let q2 = "SELECT id FROM sku_list WHERE name=? AND sku_category_id=? LIMIT 1"; let mut r = if let Some(prev) = prev { - sqlx::query(q1) - .bind(name) - .bind(category) - .bind(prev) - .fetch(&mut *tx) + sqlx::query(q1).bind(name).bind(category).bind(prev).fetch(&mut *tx) } else { sqlx::query(q2).bind(name).bind(category).fetch(&mut *tx) }; @@ -81,28 +67,24 @@ impl SKUModule { } pub async fn add(&self, mut v: SKU, tx: &mut SqliteConnection) -> Result { - let r = - sqlx::query("INSERT INTO sku_list (name, description, sku_category_id, color, text_color) VALUES(?, ?, ?, ?, ?)") - .bind(&v.name) - .bind(&v.description) - .bind(v.sku_category_id) - .bind(&v.color) - .bind(&v.text_color) - .execute(&mut *tx) - .await?; + let r = sqlx::query("INSERT INTO sku_list (name, description, sku_category_id, color, text_color) VALUES(?, ?, ?, ?, ?)") + .bind(&v.name) + .bind(&v.description) + .bind(v.sku_category_id) + .bind(&v.color) + .bind(&v.text_color) + .execute(&mut *tx) + .await?; if r.rows_affected() != 1 { bail!("Can't add sku"); } - v.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "sku_list", tx) - .await?; + v.id = sql::try_set_standard_id(r.last_insert_rowid(), "sku_list", tx).await?; self.ps.notice(WebSocketFlags::AddSKU(v.id)).await?; Ok(v) } pub async fn remove(&self, id: i64, notice: bool, tx: &mut SqliteConnection) -> Result { - let r = self.ps.remove_row_from_table(id, "sku_list", tx).await?; + let r = remove_row_from_table(id, "sku_list", tx).await?; if notice { self.ps.notice(WebSocketFlags::RemoveSKU(id)).await?; } @@ -110,15 +92,10 @@ impl SKUModule { } pub async fn get(&self, id: i64, tx: &mut SqliteConnection) -> Result> { - self.ps.get_row_from_table("sku_list", "id", id, tx).await + get_row_from_table("sku_list", "id", id, tx).await } - pub async fn get_multiple( - &self, - pagination: &Pagination, - query: &GetSKUsQuery, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple(&self, pagination: &Pagination, query: &GetSKUsQuery, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let ob = query.get_order_condition(); let rows = sqlx::query(&format!( @@ -138,15 +115,10 @@ impl SKUModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects(rows) + rows_to_objects(rows) } - pub async fn get_multiple_ids( - &self, - pagination: &Pagination, - query: &GetSKUsQuery, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple_ids(&self, pagination: &Pagination, query: &GetSKUsQuery, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let rows = sqlx::query(&format!( "SELECT @@ -163,18 +135,11 @@ impl SKUModule { pub async fn get_count(&self, query: &GetSKUsQuery, tx: &mut SqliteConnection) -> Result { let qw = query.get_where_condition(); - let row = sqlx::query(&format!("SELECT count(*) as count FROM sku_list {qw}")) - .fetch_one(&mut *tx) - .await?; + let row = sqlx::query(&format!("SELECT count(*) as count FROM sku_list {qw}")).fetch_one(&mut *tx).await?; Ok(row.get("count")) } - pub async fn update( - &self, - id: i64, - mut v: SKU, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn update(&self, id: i64, mut v: SKU, tx: &mut SqliteConnection) -> Result> { let r = sqlx::query("UPDATE sku_list SET name=?, description=?, sku_category_id=?, color=?, text_color=? WHERE id=?") .bind(&v.name) .bind(&v.description) @@ -194,13 +159,11 @@ impl SKUModule { } pub async fn is_depend_by_another(&self, id: i64, tx: &mut SqliteConnection) -> Result { - Ok( - sqlx::query("SELECT sku_id FROM order_items WHERE sku_id=? LIMIT 1") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some(), - ) + Ok(sqlx::query("SELECT sku_id FROM order_items WHERE sku_id=? LIMIT 1") + .bind(id) + .fetch(&mut *tx) + .try_next() + .await? + .is_some()) } } diff --git a/server/crates/statistical_module/Cargo.toml b/server/crates/statistical_module/Cargo.toml new file mode 100644 index 0000000..8ddd32c --- /dev/null +++ b/server/crates/statistical_module/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "statistical_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +order_module = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } +tokio = { workspace = true } +futures = { workspace = true } +ahash = { workspace = true } \ No newline at end of file diff --git a/server/src/erp/statistical_module.rs b/server/crates/statistical_module/src/lib.rs similarity index 97% rename from server/src/erp/statistical_module.rs rename to server/crates/statistical_module/src/lib.rs index fb63f8f..a007bfd 100644 --- a/server/src/erp/statistical_module.rs +++ b/server/crates/statistical_module/src/lib.rs @@ -1,25 +1,14 @@ use ahash::{HashMap, HashMapExt}; use futures::TryStreamExt; +use public_system::PublicSystem; use sqlx::{Row, SqliteConnection}; use std::{borrow::Cow, sync::Arc}; use tokio::{sync::RwLock, task::JoinHandle}; - -use crate::{ - myhelper::set_to_string, - public_system::{model::WebSocketFlags, PublicSystem}, -}; - -pub mod model; -use self::model::statistical_data::{ +use anyhow::Result; +use elerp_common::{model::{action_type::ActionType, WebSocketFlags}, order_module::model::order::{GetOrdersQuery, OrderCurrency, OrderType}, set_to_string, sql::in_or_not, statistical_module::model::statistical_data::{ GetStatisticalDataQuery, PopularSKU, SalesAmountWithCurrency, StatisticalData, StatisticalOrderCountData, StatisticalOrderData, -}; - -use super::{ - order_module::model::{GetOrdersQuery, OrderCurrency, OrderType}, - util::in_or_not, - ActionType, Result, -}; +}}; #[derive(Debug, Clone)] pub struct StatisticalModule { diff --git a/server/crates/user_system/Cargo.toml b/server/crates/user_system/Cargo.toml new file mode 100644 index 0000000..2ed6edd --- /dev/null +++ b/server/crates/user_system/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "user_system" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +public_system = { workspace = true } +elerp_common = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } +strum ={ workspace = true } +tokio ={ workspace = true } +futures ={ workspace = true } \ No newline at end of file diff --git a/server/src/user_system.rs b/server/crates/user_system/src/lib.rs similarity index 95% rename from server/src/user_system.rs rename to server/crates/user_system/src/lib.rs index 62de288..fa9a786 100644 --- a/server/src/user_system.rs +++ b/server/crates/user_system/src/lib.rs @@ -1,24 +1,23 @@ -pub mod models; - use std::{sync::Arc, time::Duration}; -use self::models::{ - user_configure::{UserConfigure, UserConfigureDefaults}, - user_info::{GetUsersQuery, UserInfo, UserType}, +use anyhow::{bail, Result}; +use elerp_common::sql::{ + get_row_from_table, is_exists_in_table, remove_row_from_table, row_is_duplicate_col_in_table, + rows_to_objects, }; -use crate::{ - erp::ActionType, - public_system::{ - model::{Pagination, WebSocketFlags}, - PublicSystem, - }, - user_system::models::user_permission::{ - ADD_ORDER, MANAGE_AREA, MANAGE_PERSON, MANAGE_SKU, MANAGE_SKU_CATEGORY, MANAGE_WAREHOUSE, - UPDATE_REMOVE_ORDER, +use elerp_common::{ + model::{action_type::ActionType, Pagination, WebSocketFlags}, + user_system::model::{ + user_configure::{UserConfigure, UserConfigureDefaults}, + user_info::{GetUsersQuery, UserInfo, UserType}, + user_permission::{ + ADD_ORDER, MANAGE_AREA, MANAGE_PERSON, MANAGE_SKU, MANAGE_SKU_CATEGORY, + MANAGE_WAREHOUSE, UPDATE_REMOVE_ORDER, + }, }, }; -use anyhow::{bail, Result}; use futures::TryStreamExt; +use public_system::PublicSystem; use sqlx::{Row, SqliteConnection}; use tokio::task::JoinHandle; use tracing::{error, info, warn}; @@ -393,7 +392,7 @@ impl UserSystem { } pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps.is_exists_in_table("users", "id", id, tx).await + is_exists_in_table("users", "id", id, tx).await } pub async fn is_exists_name( @@ -402,9 +401,7 @@ impl UserSystem { prev: Option, tx: &mut SqliteConnection, ) -> Result { - self.ps - .row_is_duplicate_col_in_table(name, prev, "users", "username", &mut *tx) - .await + row_is_duplicate_col_in_table(name, prev, "users", "username", &mut *tx).await } pub async fn add_user( @@ -440,7 +437,7 @@ impl UserSystem { } pub async fn remove_user(&self, id: i64, tx: &mut SqliteConnection) -> Result { - let r = self.ps.remove_row_from_table(id, "users", &mut *tx).await?; + let r = remove_row_from_table(id, "users", &mut *tx).await?; self.remove_configure(id, &mut *tx).await?; self.ps.notice(WebSocketFlags::RemoveUser(id)).await?; Ok(r) @@ -452,8 +449,7 @@ impl UserSystem { action: ActionType, tx: &mut SqliteConnection, ) -> Result> { - self.ps - .get_row_from_table("users", "id", id, &mut *tx) + get_row_from_table("users", "id", id, &mut *tx) .await .map(|opt| { opt.map(|mut user: UserInfo| { @@ -475,9 +471,7 @@ impl UserSystem { username: &str, tx: &mut SqliteConnection, ) -> Result> { - self.ps - .get_row_from_table("users", "username", username.to_owned(), &mut *tx) - .await + get_row_from_table("users", "username", username.to_owned(), &mut *tx).await } pub async fn get_users( @@ -493,7 +487,7 @@ impl UserSystem { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects::(rows).map(|mut items| { + rows_to_objects::(rows).map(|mut items| { for item in items.iter_mut() { match action { ActionType::General(_) | ActionType::GeneralAllowed(_) => { diff --git a/server/crates/warehouse_module/Cargo.toml b/server/crates/warehouse_module/Cargo.toml new file mode 100644 index 0000000..2f54e96 --- /dev/null +++ b/server/crates/warehouse_module/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "warehouse_module" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +sqlx = { workspace = true } +elerp_common = { workspace = true } +public_system = { workspace = true } +user_system = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +utoipa ={ workspace = true } +futures-util ={ workspace = true } \ No newline at end of file diff --git a/server/src/erp/warehouse_module.rs b/server/crates/warehouse_module/src/lib.rs similarity index 55% rename from server/src/erp/warehouse_module.rs rename to server/crates/warehouse_module/src/lib.rs index 2ba1354..9fbd6db 100644 --- a/server/src/erp/warehouse_module.rs +++ b/server/crates/warehouse_module/src/lib.rs @@ -1,22 +1,19 @@ -use crate::{ - public_system::{ - model::{Pagination, WebSocketFlags}, - PublicSystem, - }, - user_system::models::user_info::{UserInfo, UserType}, -}; - -use self::model::{ - fn_argument::{UserInfoID, WarehouseIsFrom}, - warehouse::GetWarehousesQuery, -}; - -use super::{ActionType, Result}; use anyhow::bail; -use futures::TryStreamExt; -use model::warehouse::Warehouse; +use anyhow::Result; +use elerp_common::model::action_type::ActionType; +use elerp_common::model::Pagination; +use elerp_common::model::WebSocketFlags; +use elerp_common::sql; +use elerp_common::sql::{get_row_from_table, is_exists_in_table, remove_row_from_table, row_is_duplicate_col_in_table, rows_to_objects}; +use elerp_common::user_system::model::user_info::UserInfo; +use elerp_common::user_system::model::user_info::UserType; +use elerp_common::warehouse_module::model::fn_argument::UserInfoID; +use elerp_common::warehouse_module::model::fn_argument::WarehouseIsFrom; +use elerp_common::warehouse_module::model::warehouse::GetWarehousesQuery; +use elerp_common::warehouse_module::model::warehouse::Warehouse; +use futures_util::TryStreamExt; +use public_system::PublicSystem; use sqlx::{Row, SqliteConnection}; -pub mod model; #[derive(Debug, Clone)] pub struct WarehouseModule { @@ -78,73 +75,47 @@ impl WarehouseModule { } pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { - let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM warehouses;") - .fetch_one(&mut *tx) - .await? - .get("count"); + let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM warehouses;").fetch_one(&mut *tx).await?.get("count"); Ok(count >= self.ps.get_config().limit.warehouses) } - pub async fn is_exists_name( - &self, - name: &str, - prev: Option, - tx: &mut SqliteConnection, - ) -> Result { - self.ps - .row_is_duplicate_col_in_table(name, prev, "warehouses", "name", tx) - .await + pub async fn is_exists_name(&self, name: &str, prev: Option, tx: &mut SqliteConnection) -> Result { + row_is_duplicate_col_in_table(name, prev, "warehouses", "name", tx).await } - pub async fn add( - &self, - mut warehouse: Warehouse, - tx: &mut SqliteConnection, - ) -> Result { - let r = sqlx::query( - "INSERT INTO warehouses (name, description, address, person_in_charge_id, area_id, color, text_color) VALUES(?, ?, ?, ?, ?, ?, ?)", - ) - .bind(&warehouse.name) - .bind(&warehouse.description) - .bind(&warehouse.address) - .bind(warehouse.person_in_charge_id) - .bind(warehouse.area_id) - .bind(&warehouse.color) - .bind(&warehouse.text_color) - .execute(&mut *tx) - .await?; + pub async fn add(&self, mut warehouse: Warehouse, tx: &mut SqliteConnection) -> Result { + let r = sqlx::query("INSERT INTO warehouses (name, description, address, person_in_charge_id, area_id, color, text_color) VALUES(?, ?, ?, ?, ?, ?, ?)") + .bind(&warehouse.name) + .bind(&warehouse.description) + .bind(&warehouse.address) + .bind(warehouse.person_in_charge_id) + .bind(warehouse.area_id) + .bind(&warehouse.color) + .bind(&warehouse.text_color) + .execute(&mut *tx) + .await?; if r.rows_affected() != 1 { bail!("Can't add warehouse"); } - warehouse.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "warehouses", tx) - .await?; - self.ps - .notice(WebSocketFlags::AddWarehouse(warehouse.id)) - .await?; + warehouse.id = sql::try_set_standard_id(r.last_insert_rowid(), "warehouses", tx).await?; + self.ps.notice(WebSocketFlags::AddWarehouse(warehouse.id)).await?; Ok(warehouse) } pub async fn remove(&self, warehouse_id: i64, notice: bool, tx: &mut SqliteConnection) -> Result { - let r = self - .ps - .remove_row_from_table(warehouse_id, "warehouses", tx) - .await?; + let r = remove_row_from_table(warehouse_id, "warehouses", tx).await?; if notice { - self.ps - .notice(WebSocketFlags::RemoveWarehouse(warehouse_id)) - .await?; + self.ps.notice(WebSocketFlags::RemoveWarehouse(warehouse_id)).await?; } Ok(r) } pub async fn get(&self, id: i64, tx: &mut SqliteConnection) -> Result> { - self.ps.get_row_from_table("warehouses", "id", id, tx).await + get_row_from_table("warehouses", "id", id, tx).await } pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps.is_exists_in_table("warehouses", "id", id, tx).await + is_exists_in_table("warehouses", "id", id, tx).await } fn get_permission_inner(&self, action: ActionType) -> String { @@ -155,13 +126,7 @@ impl WarehouseModule { ActionType::Admin | ActionType::System => String::new(), } } - pub async fn get_multiple( - &self, - pagination: &Pagination, - query: &GetWarehousesQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple(&self, pagination: &Pagination, query: &GetWarehousesQuery, action: ActionType, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let ob = query.get_order_condition(); let inner = self.get_permission_inner(action); @@ -187,30 +152,20 @@ impl WarehouseModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps - .rows_to_objects(rows) - .map(|mut list: Vec| { - for v in list.iter_mut() { - match action { - ActionType::General(_) => { - v.address = String::new(); - } - ActionType::Admin | ActionType::System | ActionType::GeneralAllowed(_) => { - () - } + rows_to_objects(rows).map(|mut list: Vec| { + for v in list.iter_mut() { + match action { + ActionType::General(_) => { + v.address = String::new(); } + ActionType::Admin | ActionType::System | ActionType::GeneralAllowed(_) => (), } - list - }) + } + list + }) } - pub async fn get_multiple_ids( - &self, - pagination: &Pagination, - query: &GetWarehousesQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_multiple_ids(&self, pagination: &Pagination, query: &GetWarehousesQuery, action: ActionType, tx: &mut SqliteConnection) -> Result> { let qw = query.get_where_condition(); let inner = self.get_permission_inner(action); let rows = sqlx::query(&format!( @@ -227,46 +182,28 @@ impl WarehouseModule { Ok(rows.into_iter().map(|row| row.get("id")).collect()) } - pub async fn get_count( - &self, - query: &GetWarehousesQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn get_count(&self, query: &GetWarehousesQuery, action: ActionType, tx: &mut SqliteConnection) -> Result { let qw = query.get_where_condition(); let inner = self.get_permission_inner(action); - let row = sqlx::query(&format!( - "SELECT count(*) as count FROM warehouses {inner} {qw}" - )) - .fetch_one(&mut *tx) - .await?; + let row = sqlx::query(&format!("SELECT count(*) as count FROM warehouses {inner} {qw}")).fetch_one(&mut *tx).await?; Ok(row.get("count")) } - pub async fn update( - &self, - id: i64, - mut v: Warehouse, - tx: &mut SqliteConnection, - ) -> Result> { - let r = sqlx::query( - "UPDATE warehouses SET name=?, description=?, person_in_charge_id=?, area_id=?, address=?, color=?, text_color=? WHERE id=?", - ) - .bind(&v.name) - .bind(&v.description) - .bind(v.person_in_charge_id) - .bind(v.area_id) - .bind(&v.address) - .bind(&v.color) - .bind(&v.text_color) - .bind(id) - .execute(&mut *tx) - .await?; + pub async fn update(&self, id: i64, mut v: Warehouse, tx: &mut SqliteConnection) -> Result> { + let r = sqlx::query("UPDATE warehouses SET name=?, description=?, person_in_charge_id=?, area_id=?, address=?, color=?, text_color=? WHERE id=?") + .bind(&v.name) + .bind(&v.description) + .bind(v.person_in_charge_id) + .bind(v.area_id) + .bind(&v.address) + .bind(&v.color) + .bind(&v.text_color) + .bind(id) + .execute(&mut *tx) + .await?; Ok(if r.rows_affected() == 1 { v.id = id; - self.ps - .notice(WebSocketFlags::UpdateWarehouse(v.id)) - .await?; + self.ps.notice(WebSocketFlags::UpdateWarehouse(v.id)).await?; Some(v) } else { None @@ -274,20 +211,10 @@ impl WarehouseModule { } pub async fn is_depend_by_another(&self, id: i64, tx: &mut SqliteConnection) -> Result { - Ok(sqlx::query("SELECT id FROM orders WHERE warehouse_id=?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some()) + Ok(sqlx::query("SELECT id FROM orders WHERE warehouse_id=?").bind(id).fetch(&mut *tx).try_next().await?.is_some()) } - pub async fn link( - &self, - warehouse_id: i64, - user_id: i64, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn link(&self, warehouse_id: i64, user_id: i64, tx: &mut SqliteConnection) -> Result { let is_link = sqlx::query("INSERT INTO warehouse_permission VALUES (?, ?)") .bind(warehouse_id) .bind(user_id) @@ -296,45 +223,28 @@ impl WarehouseModule { .rows_affected() == 1; if is_link { - self.ps - .notice(WebSocketFlags::LinkedWarehouse(warehouse_id)) - .await?; + self.ps.notice(WebSocketFlags::LinkedWarehouse(warehouse_id)).await?; self.ps.notice(WebSocketFlags::LinkedUser(user_id)).await?; } Ok(is_link) } - pub async fn unlink( - &self, - warehouse_id: i64, - user_id: i64, - tx: &mut SqliteConnection, - ) -> Result { - let is_unlink = - sqlx::query("DELETE FROM warehouse_permission WHERE warehouse_id=? AND user_id=?;") - .bind(warehouse_id) - .bind(user_id) - .execute(&mut *tx) - .await? - .rows_affected() - == 1; + pub async fn unlink(&self, warehouse_id: i64, user_id: i64, tx: &mut SqliteConnection) -> Result { + let is_unlink = sqlx::query("DELETE FROM warehouse_permission WHERE warehouse_id=? AND user_id=?;") + .bind(warehouse_id) + .bind(user_id) + .execute(&mut *tx) + .await? + .rows_affected() + == 1; if is_unlink { - self.ps - .notice(WebSocketFlags::UnlinkedWarehouse(warehouse_id)) - .await?; - self.ps - .notice(WebSocketFlags::UnlinkedUser(user_id)) - .await?; + self.ps.notice(WebSocketFlags::UnlinkedWarehouse(warehouse_id)).await?; + self.ps.notice(WebSocketFlags::UnlinkedUser(user_id)).await?; } Ok(is_unlink) } - pub async fn is_linked<'a>( - &self, - warehouse: WarehouseIsFrom, - user: UserInfoID<'a>, - tx: &mut SqliteConnection, - ) -> Result { + pub async fn is_linked<'a>(&self, warehouse: WarehouseIsFrom, user: UserInfoID<'a>, tx: &mut SqliteConnection) -> Result { let user_id = match &user { UserInfoID::InfoRef(info) => { if info.user_type == UserType::Admin { @@ -347,11 +257,9 @@ impl WarehouseModule { }; let query = match warehouse { - WarehouseIsFrom::ID(warehouse_id) => sqlx::query( - "SELECT user_id FROM warehouse_permission WHERE warehouse_id=? AND user_id=?;", - ) - .bind(warehouse_id) - .bind(user_id), + WarehouseIsFrom::ID(warehouse_id) => sqlx::query("SELECT user_id FROM warehouse_permission WHERE warehouse_id=? AND user_id=?;") + .bind(warehouse_id) + .bind(user_id), WarehouseIsFrom::Order(order_id) => sqlx::query( "SELECT w.user_id FROM orders o @@ -381,12 +289,7 @@ impl WarehouseModule { Ok(query.fetch(&mut *tx).try_next().await?.is_some()) } - pub async fn get_linked_users( - &self, - warehouse_id: i64, - pagination: &Pagination, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_linked_users(&self, warehouse_id: i64, pagination: &Pagination, tx: &mut SqliteConnection) -> Result> { let rows = sqlx::query(&format!( "SELECT users.* FROM users @@ -398,16 +301,11 @@ impl WarehouseModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects(rows) + rows_to_objects(rows) } #[allow(dead_code)] - pub async fn get_linked_warehouses( - &self, - user_id: i64, - pagination: &Pagination, - tx: &mut SqliteConnection, - ) -> Result> { + pub async fn get_linked_warehouses(&self, user_id: i64, pagination: &Pagination, tx: &mut SqliteConnection) -> Result> { let rows = sqlx::query(&format!( "SELECT warehouses.* FROM warehouses @@ -419,20 +317,14 @@ impl WarehouseModule { .bind(pagination.offset()) .fetch_all(&mut *tx) .await?; - self.ps.rows_to_objects(rows) + rows_to_objects(rows) } - pub async fn get_linked_users_count( - &self, - warehouse_id: i64, - tx: &mut SqliteConnection, - ) -> Result { - let row = sqlx::query(&format!( - "SELECT count(*) as count FROM warehouse_permission WHERE warehouse_id=?" - )) - .bind(warehouse_id) - .fetch_one(&mut *tx) - .await?; + pub async fn get_linked_users_count(&self, warehouse_id: i64, tx: &mut SqliteConnection) -> Result { + let row = sqlx::query(&format!("SELECT count(*) as count FROM warehouse_permission WHERE warehouse_id=?")) + .bind(warehouse_id) + .fetch_one(&mut *tx) + .await?; Ok(row.get("count")) } } diff --git a/server/src/erp/area_module/model.rs b/server/src/erp/area_module/model.rs deleted file mode 100644 index d6b4d25..0000000 --- a/server/src/erp/area_module/model.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod area; - -pub use area::*; diff --git a/server/src/erp/dependency.rs b/server/src/erp/dependency.rs deleted file mode 100644 index 02cf6df..0000000 --- a/server/src/erp/dependency.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::sync::Arc; - -use super::{ - area_module::AreaModule, guest_order_module::GuestOrderModule, inventory_module::InventoryModule, order_module::OrderModule, order_payment_module::OrderPaymentModule, order_category_module::OrderCategoryModule, person_module::PersonModule, sku_category_module::SKUCategoryModule, sku_module::SKUModule, statistical_module::StatisticalModule, warehouse_module::WarehouseModule -}; - -#[derive(Debug, Clone)] -pub struct ModuleDependency { - pub area: Arc, - pub person: Arc, - pub warehouse: Arc, - pub sku_category: Arc, - pub sku: Arc, - pub order: Arc, - pub guest_order: Arc, - pub order_category: Arc, - pub order_payment: Arc, - pub inventory: Arc, - pub statistical: Arc, -} diff --git a/server/src/erp/guest_order_module/model.rs b/server/src/erp/guest_order_module/model.rs deleted file mode 100644 index 5412f45..0000000 --- a/server/src/erp/guest_order_module/model.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod guest_order; - -pub use guest_order::*; diff --git a/server/src/erp/inventory_module.rs b/server/src/erp/inventory_module.rs deleted file mode 100644 index 65644af..0000000 --- a/server/src/erp/inventory_module.rs +++ /dev/null @@ -1,459 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use ahash::{HashMap, HashMapExt, HashSet, HashSetExt}; -use anyhow::bail; -use futures::TryStreamExt; -use sqlx::{FromRow, Row, SqliteConnection}; -use tokio::{fs, io::AsyncWriteExt, sync::RwLock}; -use tracing::warn; - -use crate::{ - erp::{ - sku_category_module::model::SKUCategory, sku_module::model::SKU, - warehouse_module::model::warehouse::Warehouse, ActionType, - }, - public_system::{model::Pagination, PublicSystem}, -}; - -use self::model::{GetInventoryQuery, InventoryProduct}; -use super::order_module::model::{OrderItem, OrderType}; -use super::{dependency::ModuleDependency, Result}; -pub mod model; - -#[derive(Debug, Clone)] -pub struct VirtualInventory { - inventory_module: InventoryModule, - inventory: HashMap<(i64, i64), Option>, -} - -#[derive(Debug, Clone)] -pub struct VirtualInventoryProduct { - pub sku_category_id: i64, - quantity: i64, - latest_quantity: i64, -} - -impl VirtualInventoryProduct { - pub fn quantity(&self) -> i64 { - self.quantity - } - - pub fn latest_quantity(&self) -> i64 { - self.latest_quantity - } - - pub fn change(&mut self, v: i64) -> i64 { - self.latest_quantity = v; - self.latest_quantity - } -} - -impl VirtualInventory { - pub fn new(inventory_module: InventoryModule, capicity: usize) -> Self { - Self { - inventory_module, - inventory: HashMap::with_capacity(capicity), - } - } - - pub async fn get_mut( - &mut self, - warehouse_id: i64, - sku_id: i64, - tx: &mut SqliteConnection, - ) -> Result> { - Ok(if self.inventory.contains_key(&(warehouse_id, sku_id)) { - let p = self.inventory.get_mut(&(warehouse_id, sku_id)).unwrap(); - p.as_mut() - } else { - let p = self - .inventory_module - .get(warehouse_id, sku_id, &mut *tx) - .await? - .map(|p| VirtualInventoryProduct { - sku_category_id: p.sku_category_id, - quantity: p.quantity, - latest_quantity: p.quantity, - }); - self.inventory.insert((warehouse_id, sku_id), p); - self.inventory - .get_mut(&(warehouse_id, sku_id)) - .unwrap() - .as_mut() - }) - } -} - -#[derive(Debug, Clone)] -pub struct InventoryModule { - ps: PublicSystem, - dependency: Arc>>, -} - -impl InventoryModule { - pub async fn set_dependency(&self, dep: ModuleDependency) { - *self.dependency.write().await = Some(dep); - } - - pub async fn create_table(&self, tx: &mut SqliteConnection) -> Result<()> { - sqlx::query( - "CREATE TABLE IF NOT EXISTS inventory( - warehouse_id INT NOT NULL, - sku_id INT NOT NULL, - sku_category_id INT NOT NULL, - quantity INT NOT NULL - )", - ) - .execute(&mut *tx) - .await - .unwrap(); - sqlx::query( - "CREATE INDEX IF NOT EXISTS inventory_warehouses - ON inventory(warehouse_id); - CREATE INDEX IF NOT EXISTS inventory_skus - ON inventory(sku_id); - CREATE INDEX IF NOT EXISTS inventory_warehouses_and_skus - ON inventory(warehouse_id, sku_id); - CREATE INDEX IF NOT EXISTS inventory_sku_categories - ON inventory(sku_category_id);", - ) - .execute(&mut *tx) - .await - .unwrap(); - - Ok(()) - } - - pub async fn new(ps: PublicSystem) -> Self { - let mut tx = ps.get_conn().begin().await.unwrap(); - let s = Self { - ps, - dependency: Arc::new(RwLock::new(None)), - }; - s.create_table(tx.as_mut()).await.unwrap(); - tx.commit().await.unwrap(); - s - } - - pub fn get_virtual(&self, capicity: usize) -> VirtualInventory { - VirtualInventory::new(self.clone(), capicity) - } - - async fn add( - &self, - warehouse_id: i64, - sku: &SKU, - quantity: i64, - tx: &mut SqliteConnection, - ) -> Result> { - let r = sqlx::query("INSERT INTO inventory (warehouse_id, sku_id, sku_category_id, quantity) VALUES (?, ?, ?, ?)") - .bind(warehouse_id) - .bind(sku.id) - .bind(sku.sku_category_id) - .bind(quantity) - .execute(&mut *tx) - .await?; - - Ok(if r.rows_affected() != 1 { - None - } else { - Some(InventoryProduct { - warehouse_id, - sku_id: sku.id, - sku_category_id: sku.sku_category_id, - quantity, - }) - }) - } - - pub async fn get( - &self, - warehouse_id: i64, - sku_id: i64, - tx: &mut SqliteConnection, - ) -> Result> { - Ok(sqlx::query_as::<_, InventoryProduct>( - "SELECT * FROM inventory WHERE warehouse_id=? AND sku_id=? LIMIT 1", - ) - .bind(warehouse_id) - .bind(sku_id) - .fetch(&mut *tx) - .try_next() - .await?) - } - - async fn update( - &self, - product: InventoryProduct, - tx: &mut SqliteConnection, - ) -> Result> { - let r = - sqlx::query("UPDATE inventory SET quantity=? WHERE warehouse_id = ? AND sku_id = ?") - .bind(product.quantity) - .bind(product.warehouse_id) - .bind(product.sku_id) - .execute(&mut *tx) - .await?; - - Ok(if r.rows_affected() != 1 { - None - } else { - Some(product) - }) - } - - pub fn calc_quantity_by_order_type( - &self, - mut inventory_quantity: i64, - item: &OrderItem, - order_type: OrderType, - ) -> i64 { - match order_type { - OrderType::Return | OrderType::StockIn => { - if !item.exchanged { - inventory_quantity += item.quantity - } - } - OrderType::StockOut => { - if !item.exchanged { - inventory_quantity -= item.quantity - } - } - OrderType::Exchange => { - if item.exchanged { - inventory_quantity += item.quantity - } else { - inventory_quantity -= item.quantity - } - } - OrderType::Calibration | OrderType::CalibrationStrict => { - if !item.exchanged { - inventory_quantity = item.quantity; - } - } - OrderType::Verification | OrderType::VerificationStrict => (), - }; - inventory_quantity - } - - pub async fn change( - &self, - warehouse_id: i64, - items: &Vec, - order_type: OrderType, - tx: &mut SqliteConnection, - ) -> Result<()> { - let depl = self.dependency.read().await; - let dep: &ModuleDependency = depl.as_ref().unwrap(); - let mut products = HashSet::with_capacity(items.len()); - let mut inventory = self.get_virtual(items.len()); - - if order_type == OrderType::CalibrationStrict { - sqlx::query("UPDATE inventory SET quantity=0 WHERE warehouse_id=?") - .bind(warehouse_id) - .execute(&mut *tx) - .await?; - } - for item in items { - if item.exchanged && order_type != OrderType::Exchange { - continue; - } - if let Some(sku) = dep.sku.get(item.sku_id, tx).await? { - match inventory.get_mut(warehouse_id, item.sku_id, tx).await? { - Some(product) => { - product.change(self.calc_quantity_by_order_type( - product.latest_quantity(), - item, - order_type, - )); - products.insert(item.sku_id); - } - None => { - let produtc_quantity = - self.calc_quantity_by_order_type(0, item, order_type); - self.add(warehouse_id, &sku, produtc_quantity, tx) - .await? - .expect("Can't add the new inventory!"); - } - } - } else { - bail!("SKU not found!") - } - } - for sku_id in products { - let vproduct = inventory - .get_mut(warehouse_id, sku_id, tx) - .await? - .expect("It must contain because called get_mut() before"); - let product = InventoryProduct { - warehouse_id, - sku_id, - sku_category_id: vproduct.sku_category_id, - quantity: vproduct.latest_quantity(), - }; - if self.update(product, tx).await?.is_none() { - warn!("Can't update the specified product by id {}!", sku_id); - bail!("Please ensure order is correct!"); - } - } - - Ok(()) - } - - fn get_permission_inner(&self, action: ActionType) -> String { - match action { - ActionType::General(id) | ActionType::GeneralAllowed(id) => { - format!("INNER JOIN warehouse_permission ON warehouse_permission.user_id={id} AND warehouse_permission.warehouse_id=inventory.warehouse_id") - } - ActionType::Admin | ActionType::System => String::new(), - } - } - - const SELECT: &'static str = "SELECT - inventory.warehouse_id, - inventory.sku_id, - inventory.sku_category_id, - inventory.quantity, - - warehouses.name AS warehouse_name, - sku_list.name AS sku_name, - sku_categories.name AS sku_category_name - - FROM inventory - INNER JOIN warehouses ON inventory.warehouse_id=warehouses.id - INNER JOIN sku_list ON inventory.sku_id=sku_list.id - INNER JOIN sku_categories ON inventory.sku_category_id=sku_categories.id"; - - pub async fn list( - &self, - pagination: &Pagination, - query: &GetInventoryQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { - let select = Self::SELECT; - let qw = query.get_where_condition(); - let ob = query.get_order_condition(); - let inner = self.get_permission_inner(action); - let rows = sqlx::query(&format!("{select} {inner} {qw} {ob} LIMIT ? OFFSET ?")) - .bind(pagination.limit()) - .bind(pagination.offset()) - .fetch_all(&mut *tx) - .await?; - let mut arr = Vec::with_capacity(rows.len()); - for row in rows { - arr.push(InventoryProduct::from_row(&row).unwrap()) - } - - Ok(arr) - } - - pub async fn get_excel( - &self, - query: &GetInventoryQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result { - use rust_xlsxwriter::{Color, Format, FormatAlign, FormatBorder, Workbook, Worksheet}; - - let inner = self.get_permission_inner(action); - let qw = query.get_where_condition(); - let ob = query.get_order_condition(); - let rows = sqlx::query(&format!("SELECT * FROM inventory {inner} {qw} {ob}")) - .fetch_all(&mut *tx) - .await?; - let mut arr = Vec::with_capacity(rows.len()); - for row in rows { - arr.push(InventoryProduct::from_row(&row).unwrap()) - } - - // Create a new Excel file object. - let mut workbook = Workbook::new(); - // Create some formats to use in the worksheet. - let header_format = Format::new() - .set_background_color(Color::Theme(4, 0)) - .set_font_color(Color::Theme(0, 0)) - .set_border(FormatBorder::Thin) - .set_bold() - .set_align(FormatAlign::Center) - .set_align(FormatAlign::VerticalCenter); - let data_format = Format::new() - .set_border(FormatBorder::Thin) - .set_align(FormatAlign::Center) - .set_align(FormatAlign::VerticalCenter); - - // Add a worksheet to the workbook. - let worksheet: &mut Worksheet = workbook.add_worksheet(); - worksheet.write_row_with_format( - 0, - 0, - ["Warehouse", "SKU", "SKU Category", "Quantity"], - &header_format, - )?; - let depl = self.dependency.read().await; - let dep = depl.as_ref().unwrap(); - let mut warehouses: HashMap = HashMap::new(); - let mut skus: HashMap = HashMap::new(); - let mut sku_categories: HashMap = HashMap::new(); - for (i, p) in arr.iter().enumerate() { - let warehouse = warehouses - .entry(p.warehouse_id) - .or_insert(dep.warehouse.get(p.warehouse_id, tx).await?.unwrap()); - let sku = skus - .entry(p.sku_id) - .or_insert(dep.sku.get(p.sku_id, tx).await?.unwrap()); - let sku_category = sku_categories - .entry(p.sku_category_id) - .or_insert(dep.sku_category.get(p.sku_category_id, tx).await?.unwrap()); - - let row = (i + 1) as u32; - worksheet.write_row_with_format( - row, - 0, - [&warehouse.name, &sku.name, &sku_category.name], - &data_format, - )?; - worksheet.write_with_format(row, 3, p.quantity, &data_format)?; - } - let excels = self.ps.get_data_path().join("excels").join("inventory"); - if !excels.is_dir() { - fs::create_dir_all(&excels).await?; - } - let path = excels.join(format!( - "inventory-{}.xlsx", - self.ps.get_timestamp_seconds() - )); - if path.is_file() { - fs::remove_file(&path).await?; - } - let mut file = fs::File::create(&path).await?; - let buffer = workbook.save_to_buffer()?; - file.write_all(&buffer).await?; - Ok(path) - } - - pub async fn clear_cache(&self) -> Result<()> { - let excels = self.ps.get_data_path().join("excels"); - if excels.is_dir() { - fs::remove_dir_all(&excels).await?; - } - Ok(()) - } - - pub async fn get_count( - &self, - query: &GetInventoryQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result { - let s = Self::SELECT; - let qw = query.get_where_condition(); - let inner = self.get_permission_inner(action); - let row = sqlx::query(&format!( - "SELECT count(*) as count FROM ({s} {inner} {qw}) AS tbl" - )) - .fetch_one(&mut *tx) - .await?; - Ok(row.get("count")) - } -} diff --git a/server/src/erp/inventory_module/model.rs b/server/src/erp/inventory_module/model.rs deleted file mode 100644 index 38a69db..0000000 --- a/server/src/erp/inventory_module/model.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod inventory; - -pub use inventory::*; diff --git a/server/src/erp/order_category_module/model.rs b/server/src/erp/order_category_module/model.rs deleted file mode 100644 index d2713b0..0000000 --- a/server/src/erp/order_category_module/model.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod order_category; -pub use order_category::*; diff --git a/server/src/erp/order_module.rs b/server/src/erp/order_module.rs deleted file mode 100644 index b6fb80d..0000000 --- a/server/src/erp/order_module.rs +++ /dev/null @@ -1,923 +0,0 @@ -use ahash::{HashMap, HashMapExt, HashSet, HashSetExt}; -use anyhow::bail; -use futures::TryStreamExt; -use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, SqliteConnection}; -use std::sync::Arc; -use tokio::sync::RwLock; - -use crate::{ - myhelper::set_to_string, - public_system::{ - model::{Pagination, WebSocketFlags}, - PublicSystem, - }, - user_system::models::user_info::{UserInfo, UserType}, -}; - -use self::model::{ - CheckOrderResult, GetOrdersQuery, ItemNotAvailable, Order, OrderCurrency, OrderItem, - OrderPaymentStatus, OrderType, -}; -use super::{ - dependency::ModuleDependency, warehouse_module::model::warehouse::GetWarehousesQuery, - ActionType, Result, -}; - -pub mod model; - -#[derive(Debug, Clone)] -pub struct OrderModule { - ps: PublicSystem, - dependency: Arc>>, -} - -impl OrderModule { - pub async fn set_dependency(&self, dep: ModuleDependency) { - *self.dependency.write().await = Some(dep); - } - pub async fn new(ps: PublicSystem) -> Self { - let mut tx = ps.begin_tx(true).await.unwrap(); - - sqlx::query( - "CREATE TABLE IF NOT EXISTS orders( - id INTEGER PRIMARY KEY, - from_guest_order_id INT NOT NULL, - created_by_user_id INT NOT NULL, - updated_by_user_id INT NOT NULL, - warehouse_id INT NOT NULL, - total_amount REAL NOT NULL, - total_amount_settled REAL NOT NULL, - order_payment_status TEXT NOT NULL, - currency TEXT NOT NULL, - person_related_id INT NOT NULL, - person_in_charge_id INT NOT NULL, - date INT NOT NULL, - last_updated_date INT NOT NULL, - description TEXT NOT NULL, - order_type TEXT NOT NULL, - order_category_id INT NOT NULL - )", - ) - .execute(tx.as_mut()) - .await - .unwrap(); - - sqlx::query( - "CREATE INDEX IF NOT EXISTS order_warehouses - ON orders(warehouse_id); - CREATE INDEX IF NOT EXISTS order_currencies - ON orders(currency); - CREATE INDEX IF NOT EXISTS order_person_related_ids - ON orders(person_related_id); - CREATE INDEX IF NOT EXISTS order_person_in_charge_ids - ON orders(person_in_charge_id); - CREATE INDEX IF NOT EXISTS order_order_types - ON orders(order_type); - CREATE INDEX IF NOT EXISTS order_order_category_ids - ON orders(order_category_id);", - ) - .execute(tx.as_mut()) - .await - .unwrap(); - - sqlx::query( - "CREATE TABLE IF NOT EXISTS order_items( - order_id INT NOT NULL, - sku_id INT NOT NULL, - sku_category_id INT NOT NULL, - quantity INT NOT NULL, - price REAL NOT NULL, - amount REAL NOT NULL, - exchanged BOOLEAN NOT NULL - )", - ) - .execute(tx.as_mut()) - .await - .unwrap(); - sqlx::query( - "CREATE INDEX IF NOT EXISTS order_items_order_ids - ON order_items(order_id); - CREATE INDEX IF NOT EXISTS order_items_skus - ON order_items(sku_id); - CREATE INDEX IF NOT EXISTS order_items_sku_categories - ON order_items(sku_category_id); - CREATE INDEX IF NOT EXISTS order_items_exchanged - ON order_items(exchanged);", - ) - .execute(tx.as_mut()) - .await - .unwrap(); - - let s = Self { - ps: ps.clone(), - dependency: Arc::new(RwLock::new(None)), - }; - - tx.commit().await.unwrap(); - s - } - - pub async fn get_order_payment_status( - &self, - id: i64, - tx: &mut SqliteConnection, - ) -> Result> { - Ok( - sqlx::query("SELECT order_payment_status FROM orders WHERE id = ?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - .map(|row| row.get("order_payment_status")), - ) - } - - pub async fn is_exists(&self, id: i64, tx: &mut SqliteConnection) -> Result { - self.ps.is_exists_in_table("orders", "id", id, tx).await - } - - pub async fn is_from_guest_order(&self, id: i64, tx: &mut SqliteConnection) -> Result { - if let Some(row) = sqlx::query("SELECT from_guest_order_id FROM orders WHERE id=?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - { - let guest_id: i64 = row.get("from_guest_order_id"); - Ok(guest_id > 0) - } else { - Ok(false) - } - } - - pub async fn is_limit_reached(&self, tx: &mut SqliteConnection) -> Result { - let count: i64 = sqlx::query("SELECT COUNT(*) as count FROM orders;") - .fetch_one(&mut *tx) - .await? - .get("count"); - Ok(count >= self.ps.get_config().limit.orders) - } - - fn calc(&self, items: &Vec) -> f64 { - let mut total: f64 = 0.0; - for item in items.iter() { - if item.exchanged { - continue; - } - total += (item.quantity as f64) * item.price; - } - total - } - - pub fn preprocess( - &self, - order: &mut Order, - user: &UserInfo, - initial: bool, - person_in_charge_id: i64, - ) { - if let Some(items) = order.items.clone() { - order.items = Some( - items - .into_iter() - .filter(|item| { - let pass_quantity = match order.order_type { - OrderType::StockIn | OrderType::StockOut | OrderType::Return => { - item.quantity > 0 - } - _ => true, - }; - let pass_exchange = if order.order_type != OrderType::Exchange { - !item.exchanged - } else { - true - }; - let pass_sku_not_empty = item.sku_id > 0; - - pass_quantity && pass_exchange && pass_sku_not_empty - }) - .collect(), - ); - }; - - let now = self.ps.get_timestamp_seconds() as i64; - order.updated_by_user_id = user.id; - if initial { - order.date = now; - order.created_by_user_id = user.id; - } - order.last_updated_date = now; - order.person_in_charge_id = person_in_charge_id; - order.from_guest_order_id = 0; - } - - pub async fn can_access( - &self, - id: i64, - user: &UserInfo, - tx: &mut SqliteConnection, - ) -> Result { - Ok(user.user_type == UserType::Admin - || sqlx::query("SELECT id FROM orders WHERE id=? AND created_by_user_id=? LIMIT 1") - .bind(id) - .bind(user.id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some()) - } - - pub async fn add(&self, mut order: Order, tx: &mut SqliteConnection) -> Result { - let depl = self.dependency.read().await; - let dep = depl.as_ref().unwrap(); - let items = order.items.as_ref(); - let total_amount = if let Some(items) = items { - dep.inventory - .change(order.warehouse_id, items, order.order_type, tx) - .await?; - self.calc(items) - } else { - 0.0 - }; - let order_payment_status = if order.order_type == OrderType::StockOut && total_amount > 0.0 - { - OrderPaymentStatus::Unsettled - } else { - OrderPaymentStatus::None - }; - let r = sqlx::query("INSERT INTO orders (from_guest_order_id, created_by_user_id, updated_by_user_id, warehouse_id, currency, total_amount, person_related_id, person_in_charge_id, date, last_updated_date, description, order_type, order_category_id, total_amount_settled, order_payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") - .bind(order.from_guest_order_id) - .bind(order.created_by_user_id) - .bind(order.updated_by_user_id) - .bind(order.warehouse_id) - .bind(&order.currency) - .bind(total_amount) - .bind(order.person_related_id) - .bind(order.person_in_charge_id) - .bind(order.date) - .bind(order.last_updated_date) - .bind(&order.description) - .bind(&order.order_type) - .bind(self.ps.get_standard_id(1)) - .bind(0) - .bind(order_payment_status) - .execute(&mut *tx) - .await?; - if r.rows_affected() != 1 { - bail!("Can't insert the order to history!"); - } - - order.id = self - .ps - .try_set_standard_id(r.last_insert_rowid(), "orders", tx) - .await?; - order.total_amount = total_amount; - order.total_amount_settled = 0.0; - order.order_payment_status = order_payment_status; - - self.add_order_items(&order, tx).await?; - - self.ps.notice(WebSocketFlags::AddOrder(order.id)).await?; - Ok(order) - } - - async fn add_order_items(&self, order: &Order, tx: &mut SqliteConnection) -> Result<()> { - let items = order.items.as_ref(); - if items.is_none() || items.as_ref().unwrap().len() == 0 { - return Ok(()); - } - let mut query_builder = QueryBuilder::new("INSERT INTO order_items (order_id, sku_id, sku_category_id, quantity, price, exchanged, amount) "); - query_builder.push_values(items.unwrap(), |mut b, item| { - b.push_bind(order.id) - .push_bind(item.sku_id) - .push(format!( - "(SELECT sku_category_id FROM sku_list WHERE id = {})", - item.sku_id - )) - .push_bind(item.quantity) - .push_bind(item.price) - .push_bind(item.exchanged) - .push_bind(item.quantity as f64 * item.price); - }); - let query = query_builder.build(); - query.execute(&mut *tx).await?; - Ok(()) - } - - async fn exists_order_type( - &self, - ot: OrderType, - order_start: &Order, - tx: &mut SqliteConnection, - ) -> Result { - Ok( - sqlx::query("SELECT id FROM orders WHERE order_type=? AND date>=? AND id<>? LIMIT 1") - .bind(ot) - .bind(order_start.date) - .bind(order_start.id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some(), - ) - } - - pub async fn recall( - &self, - order: Order, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result { - let depl = self.dependency.read().await; - let dep = depl.as_ref().unwrap(); - if !self - .exists_order_type(OrderType::Calibration, &order, tx) - .await? - { - let warehouse_id = order.warehouse_id; - let mut items = self - .get_order_items(order.id, &Pagination::max(), tx) - .await?; - match &order.order_type { - OrderType::Return | OrderType::StockIn => { - dep.inventory - .change(warehouse_id, &items, OrderType::StockOut, tx) - .await?; - } - OrderType::StockOut => { - dep.inventory - .change(warehouse_id, &items, OrderType::StockIn, tx) - .await?; - } - OrderType::Exchange => { - for item in items.iter_mut() { - item.exchanged = !item.exchanged; - } - dep.inventory - .change(warehouse_id, &items, OrderType::Exchange, tx) - .await?; - } - OrderType::Calibration => { - let mut skus = HashSet::with_capacity(items.len()); - for item in items { - skus.insert(item.sku_id); - } - - self.recalc_all( - Some(HashSet::from_iter([order.warehouse_id])), - Some(skus), - Some(&order), - action, - tx, - ) - .await?; - } - OrderType::CalibrationStrict => { - self.recalc_all( - Some(HashSet::from_iter([order.warehouse_id])), - None, - Some(&order), - action, - tx, - ) - .await?; - } - OrderType::Verification | OrderType::VerificationStrict => (), - } - } - Ok(true) - } - - pub async fn recalc_all( - &self, - warehouse_ids: Option>, - skus_filter: Option>, - to_remove: Option<&Order>, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result<()> { - let depl = self.dependency.read().await; - let dep = depl.as_ref().unwrap(); - match skus_filter { - Some(skus) => { - let skus = set_to_string(&skus, ","); - match &warehouse_ids { - Some(ids) => { - let ids = set_to_string(&ids, ","); - sqlx::query(&format!("UPDATE inventory SET quantity=0 WHERE warehouse_id IN ({ids}) AND sku_id IN ({skus})")) - .execute(&mut *tx) - .await?; - } - None => { - sqlx::query("UPDATE inventory SET quantity=0") - .execute(&mut *tx) - .await?; - } - } - } - None => match &warehouse_ids { - Some(ids) => { - let ids = set_to_string(&ids, ","); - sqlx::query(&format!( - "UPDATE inventory SET quantity=0 WHERE warehouse_id IN ({ids})" - )) - .execute(&mut *tx) - .await?; - } - None => { - sqlx::query("UPDATE inventory SET quantity=0") - .execute(&mut *tx) - .await?; - } - }, - } - - let mut p = Pagination::new(0, 100); - - let warehouse_count = if warehouse_ids.is_none() { - dep.warehouse - .get_count( - &GetWarehousesQuery { - id: None, - name: None, - person_in_charge_id: None, - address: None, - area_id: None, - sorters: None, - }, - action, - &mut *tx, - ) - .await? - } else { - 1 - }; - - let mut q = GetOrdersQuery::empty(); - q.sorters = Some(vec!["date".to_owned()]); - q.warehouse_ids = warehouse_ids; - - let order_total = self.get_count(&q, action, tx).await?; - - let mut temp = HashMap::>::with_capacity(warehouse_count as _); - while p.offset() < order_total { - let mut orders = self - .get_multiple(p.next(), &q, ActionType::System, tx) - .await?; - for order in orders.iter_mut() { - if let Some(to_remove) = to_remove { - if to_remove.id == order.id { - continue; - } - } - let items = self - .get_order_items(order.id, &Pagination::max(), tx) - .await?; - for item in &items { - let it = temp - .entry(order.warehouse_id) - .or_insert(HashMap::with_capacity(items.len())); - let qty = it.entry(item.sku_id).or_insert(0); - - *qty = dep - .inventory - .calc_quantity_by_order_type(*qty, item, order.order_type); - } - } - } - for (warehouse_id, items) in temp { - let calibration_items: Vec = items - .into_iter() - .map(|(sku_id, quantity)| OrderItem { - sku_id, - quantity, - price: 0.0, - exchanged: false, - }) - .collect(); - dep.inventory - .change( - warehouse_id, - &calibration_items, - OrderType::Calibration, - &mut *tx, - ) - .await?; - } - self.ps.notice(WebSocketFlags::RecalcOrders).await?; - Ok(()) - } - - pub async fn remove( - &self, - id: i64, - recall: bool, - notice: bool, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result { - if let Some(order) = self.get(id, tx).await? { - let recalled = if recall { - self.recall(order, action, tx).await? - } else { - true - }; - if recalled { - self.ps.remove_row_from_table(id, "orders", tx).await?; - let r = sqlx::query("DELETE FROM order_items WHERE order_id=?") - .bind(id) - .execute(&mut *tx) - .await?; - if notice { - self.ps.notice(WebSocketFlags::RemoveOrder(id)).await?; - } - return Ok(r.rows_affected() > 0); - } - } - Ok(false) - } - - fn row_to_order(&self, row: SqliteRow) -> Order { - let id = row.get("id"); - Order { - id, - from_guest_order_id: row.get("from_guest_order_id"), - created_by_user_id: row.get("created_by_user_id"), - updated_by_user_id: row.get("updated_by_user_id"), - currency: row.try_get("currency").unwrap_or(OrderCurrency::Unknown), - total_amount: row.get("total_amount"), - total_amount_settled: row.get("total_amount_settled"), - order_payment_status: row.get("order_payment_status"), - warehouse_id: row.get("warehouse_id"), - person_related_id: row.get("person_related_id"), - person_in_charge_id: row.get("person_in_charge_id"), - date: row.get("date"), - last_updated_date: row.get("last_updated_date"), - description: row.get("description"), - order_type: row.get("order_type"), - order_category_id: row.get("order_category_id"), - items: None, - } - } - - pub async fn get(&self, id: i64, tx: &mut SqliteConnection) -> Result> { - let r = sqlx::query("SELECT * FROM orders WHERE id = ?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await?; - Ok(if let Some(row) = r { - Some(self.row_to_order(row)) - } else { - None - }) - } - - pub async fn get_order_items( - &self, - order_id: i64, - pagination: &Pagination, - tx: &mut SqliteConnection, - ) -> Result> { - let rows = sqlx::query("SELECT * FROM order_items WHERE order_id=? LIMIT ? OFFSET ?") - .bind(order_id) - .bind(pagination.limit()) - .bind(pagination.offset()) - .fetch_all(&mut *tx) - .await?; - let arr = self.ps.rows_to_objects(rows)?; - Ok(arr) - } - - fn get_permission_inner(&self, action: ActionType) -> String { - match action { - ActionType::General(id) | ActionType::GeneralAllowed(id) => { - format!("INNER JOIN warehouse_permission - ON warehouse_permission.user_id={id} AND warehouse_permission.warehouse_id=orders.warehouse_id") - } - ActionType::Admin | ActionType::System => String::new(), - } - } - const SELECT_MULTIPLE: &'static str = " - SELECT - orders.id, - orders.from_guest_order_id, - orders.created_by_user_id, - orders.updated_by_user_id, - orders.description, - orders.date, - orders.last_updated_date, - orders.person_related_id, - orders.person_in_charge_id, - orders.warehouse_id, - orders.currency, - orders.order_type, - orders.order_category_id, - orders.total_amount, - orders.total_amount_settled, - orders.order_payment_status, - persons_related.name AS person_related_name, - COALESCE(persons_in_charge.name, 'Empty') AS person_in_charge_name, - warehouses.name AS warehouse_name, - order_categories.name AS status_name - FROM orders - "; - const INNERS: &'static str = " - INNER JOIN persons AS persons_related ON orders.person_related_id=persons_related.id - LEFT JOIN persons AS persons_in_charge ON orders.person_in_charge_id=persons_in_charge.id - INNER JOIN order_categories ON orders.order_category_id=order_categories.id - INNER JOIN warehouses ON orders.warehouse_id=warehouses.id - "; - - pub async fn get_multiple( - &self, - pagination: &Pagination, - query: &GetOrdersQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { - let s = Self::SELECT_MULTIPLE; - let inners = Self::INNERS; - let qw = query.get_where_condition(); - let ob = query.get_order_condition(); - - let inner = self.get_permission_inner(action); - - let rows = sqlx::query(&format!("{s} {inners} {inner} {qw} {ob} LIMIT ? OFFSET ?")) - .bind(pagination.limit()) - .bind(pagination.offset()) - .fetch_all(&mut *tx) - .await?; - let mut arr = Vec::with_capacity(rows.len()); - for row in rows { - arr.push(self.row_to_order(row)) - } - - Ok(arr) - } - - pub async fn get_multiple_ids( - &self, - pagination: &Pagination, - query: &GetOrdersQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { - let qw = query.get_where_condition(); - - let inner = self.get_permission_inner(action); - - let rows = sqlx::query(&format!( - "SELECT id FROM orders {inner} {qw} LIMIT ? OFFSET ?" - )) - .bind(pagination.limit()) - .bind(pagination.offset()) - .fetch_all(&mut *tx) - .await?; - let mut arr = Vec::with_capacity(rows.len()); - for row in rows { - arr.push(row.get("id")) - } - - Ok(arr) - } - - pub async fn get_count( - &self, - query: &GetOrdersQuery, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result { - let s = Self::SELECT_MULTIPLE; - let inners = Self::INNERS; - let qw = query.get_where_condition(); - let inner = self.get_permission_inner(action); - let row = sqlx::query(&format!( - "SELECT count(*) as count FROM ({s} {inners} {inner} {qw}) AS tbl" - )) - .fetch_one(&mut *tx) - .await?; - Ok(row.get("count")) - } - - pub async fn is_check_pass(&self, order: &Order, tx: &mut SqliteConnection) -> Result { - Ok(self - .check(order, true, tx) - .await? - .items_not_available - .is_empty()) - } - - pub async fn check( - &self, - order: &Order, - fast_check: bool, - tx: &mut SqliteConnection, - ) -> Result { - let mut items_not_available = Vec::new(); - let items = if let Some(items) = order.items.as_ref() { - items - } else { - return Ok(CheckOrderResult { - items_not_available: vec![], - }); - }; - let mut item_map = HashMap::with_capacity(items.len()); - for item in items { - if !item.exchanged { - item_map - .entry(item.sku_id) - .and_modify(|q| *q += item.quantity) - .or_insert(item.quantity); - } - } - let mut inventory = { - let depl = self.dependency.read().await; - let dep = depl.as_ref().unwrap(); - dep.inventory.get_virtual(items.len()) - }; - match order.order_type { - OrderType::Exchange | OrderType::StockOut => { - for (sku_id, require_quantity) in item_map { - let (latest_quantity, actual_quantity) = inventory - .get_mut(order.warehouse_id, sku_id, tx) - .await? - .map(|p| { - ( - p.change(p.latest_quantity() - require_quantity), - p.quantity(), - ) - }) - .unwrap_or((0 - require_quantity, 0)); - if latest_quantity < 0 { - items_not_available.push(ItemNotAvailable { - sku_id, - require_quantity, - actual_quantity, - }); - if fast_check { - return Ok(CheckOrderResult { - items_not_available, - }); - } - } - } - } - OrderType::StockIn - | OrderType::Return - | OrderType::Calibration - | OrderType::CalibrationStrict => (), - OrderType::Verification => { - for (sku_id, require_quantity) in item_map { - let actual_quantity = inventory - .get_mut(order.warehouse_id, sku_id, tx) - .await? - .map(|p| p.quantity()) - .unwrap_or(0); - if actual_quantity != require_quantity { - items_not_available.push(ItemNotAvailable { - sku_id, - require_quantity, - actual_quantity, - }); - if fast_check { - return Ok(CheckOrderResult { - items_not_available, - }); - } - } - } - } - OrderType::VerificationStrict => { - let mut item_ids = Vec::with_capacity(item_map.len()); - for (sku_id, require_quantity) in item_map { - item_ids.push(sku_id); - let actual_quantity = inventory - .get_mut(order.warehouse_id, sku_id, tx) - .await? - .map(|p| p.quantity()) - .unwrap_or(0); - if actual_quantity != require_quantity { - items_not_available.push(ItemNotAvailable { - sku_id, - require_quantity, - actual_quantity, - }); - if fast_check { - return Ok(CheckOrderResult { - items_not_available, - }); - } - } - } - let ids = item_ids - .into_iter() - .map(|n| n.to_string()) - .collect::>() - .join(","); - - if fast_check { - if let Some(row) = sqlx::query(&format!("SELECT sku_id, quantity FROM inventory WHERE warehouse_id=? AND sku_id NOT IN ({ids}) AND quantity <> 0 LIMIT 1")) - .bind(order.warehouse_id) - .fetch(&mut *tx) - .try_next() - .await? - { - items_not_available.push(ItemNotAvailable { - sku_id: row.get("sku_id"), - require_quantity: 0, - actual_quantity: row.get("quantity"), - }); - return Ok(CheckOrderResult { - items_not_available, - }); - } - } else { - let q = format!("SELECT sku_id, quantity FROM inventory WHERE warehouse_id=? AND sku_id NOT IN ({ids}) AND quantity <> 0"); - let mut r = sqlx::query(&q).bind(order.warehouse_id).fetch(&mut *tx); - while let Some(row) = r.try_next().await? { - items_not_available.push(ItemNotAvailable { - sku_id: row.get("sku_id"), - require_quantity: 0, - actual_quantity: row.get("quantity"), - }); - } - } - } - } - Ok(CheckOrderResult { - items_not_available, - }) - } - - pub async fn update( - &self, - id: i64, - mut v: Order, - action: ActionType, - tx: &mut SqliteConnection, - ) -> Result> { - let r = match action { - ActionType::GeneralAllowed(_)| - ActionType::General(_) => sqlx::query( - "UPDATE orders SET updated_by_user_id=?, last_updated_date=?, person_related_id=?, person_in_charge_id=?, description=?, currency=?, order_category_id=? WHERE id=?", - ) - .bind(v.updated_by_user_id) - .bind(v.last_updated_date) - .bind(v.person_related_id) - .bind(v.person_in_charge_id) - .bind(&v.description) - .bind(&v.currency) - .bind(v.order_category_id) - .bind(id) - .execute(&mut *tx) - .await?, - ActionType::Admin => sqlx::query( - "UPDATE orders SET updated_by_user_id=?, last_updated_date=?, date=?, person_related_id=?, person_in_charge_id=?, description=?, currency=?, order_category_id=? WHERE id=?", - ) - .bind(v.updated_by_user_id) - .bind(v.last_updated_date) - .bind(v.date) - .bind(v.person_related_id) - .bind(v.person_in_charge_id) - .bind(&v.description) - .bind(&v.currency) - .bind(v.order_category_id) - .bind(id) - .execute(&mut *tx) - .await?, - ActionType::System => sqlx::query( - "UPDATE orders SET date=?, person_related_id=?, person_in_charge_id=?, description=?, currency=?, order_category_id=?, total_amount_settled=?, order_payment_status=? WHERE id=?", - ) - .bind(v.date) - .bind(v.person_related_id) - .bind(v.person_in_charge_id) - .bind(&v.description) - .bind(&v.currency) - .bind(v.order_category_id) - .bind(v.total_amount_settled) - .bind(v.order_payment_status) - .bind(id) - .execute(&mut *tx) - .await?, - }; - Ok(if r.rows_affected() == 1 { - v.id = id; - self.ps.notice(WebSocketFlags::UpdateOrder(v.id)).await?; - Some(v) - } else { - None - }) - } - - pub async fn is_depend_by_another(&self, id: i64, tx: &mut SqliteConnection) -> Result { - Ok( - sqlx::query("SELECT id FROM order_payments WHERE order_id=?") - .bind(id) - .fetch(&mut *tx) - .try_next() - .await? - .is_some(), - ) - } -} diff --git a/server/src/erp/order_module/model.rs b/server/src/erp/order_module/model.rs deleted file mode 100644 index 1c61643..0000000 --- a/server/src/erp/order_module/model.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod check_order_result; -pub mod order; - -pub use check_order_result::*; -pub use order::*; diff --git a/server/src/erp/order_payment_module/model.rs b/server/src/erp/order_payment_module/model.rs deleted file mode 100644 index 9439f58..0000000 --- a/server/src/erp/order_payment_module/model.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod order_payment; -pub use order_payment::*; diff --git a/server/src/erp/person_module/model.rs b/server/src/erp/person_module/model.rs deleted file mode 100644 index 7173ebd..0000000 --- a/server/src/erp/person_module/model.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod person; - -pub use person::*; diff --git a/server/src/erp/sku_category_module/model.rs b/server/src/erp/sku_category_module/model.rs deleted file mode 100644 index 50d05cc..0000000 --- a/server/src/erp/sku_category_module/model.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod sku_category; - -pub use sku_category::*; diff --git a/server/src/erp/sku_module/model.rs b/server/src/erp/sku_module/model.rs deleted file mode 100644 index b71effc..0000000 --- a/server/src/erp/sku_module/model.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod sku; - -pub use sku::*; diff --git a/server/src/erp/util.rs b/server/src/erp/util.rs deleted file mode 100644 index 8fc2252..0000000 --- a/server/src/erp/util.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::sync::OnceLock; - -use ahash::HashSet; -use regex::Regex; - -static RE: OnceLock = OnceLock::new(); - -pub fn get_search_where_condition(col: &str, query: &str) -> String { - let re: &Regex = RE.get_or_init(|| Regex::new(r"[\s+\(\)\-\:\@()]").unwrap()); - let mut tmp = [0u8; 4]; - let qs: Vec<&str> = query - .trim() - .split(|a: char| re.is_match(a.encode_utf8(&mut tmp))) - .collect(); - let mut conditions = Vec::with_capacity(qs.len()); - for q in qs { - conditions.push(format!("{col} LIKE '%{q}%'")); - } - conditions.join(" AND ").into() -} - -pub fn get_sorter_str(sorter: &str) -> &'static str { - if sorter.contains(":descend") { - "DESC" - } else { - "ASC" - } -} - -pub fn get_sort_col_str(col: &str) -> String { - col.replace(":ascend", "").replace(":descend", "").into() -} - -pub fn eq_or_not(reverse: Option<&HashSet>, col: &str) -> &'static str { - match reverse { - Some(reverse) => { - if reverse.contains(col) { - "<>" - } else { - "=" - } - } - None => "=", - } -} - -pub fn in_or_not(reverse: Option<&HashSet>, col: &str) -> &'static str { - match reverse { - Some(reverse) => { - if reverse.contains(col) { - " NOT IN " - } else { - " IN " - } - } - None => " IN ", - } -} - -pub fn exists_or_not(reverse: Option<&HashSet>, col: &str) -> &'static str { - match reverse { - Some(reverse) => { - if reverse.contains(col) { - " NOT EXISTS " - } else { - " EXISTS " - } - } - None => " EXISTS ", - } -} - -pub fn like_or_not(reverse: Option<&HashSet>, col: &str) -> &'static str { - match reverse { - Some(reverse) => { - if reverse.contains(col) { - " NOT LIKE " - } else { - " LIKE " - } - } - None => " LIKE ", - } -} - diff --git a/server/src/main.rs b/server/src/main.rs index 52e2316..8446881 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,19 +1,7 @@ -mod config; -mod custom_error; -pub mod db; -mod erp; -mod meta; -mod myhelper; -mod public_system; -mod services; -pub mod user_system; - -use clap::Parser; +use elerp_common::{config::AppConfig, meta}; pub use tracing::info; use tracing::warn; -use crate::{config::AppConfig, meta::MetaInfo}; - #[tokio::main] async fn main() { // initialize tracing @@ -24,31 +12,27 @@ async fn main() { tracing::Level::INFO }) .init(); - let meta = MetaInfo::parse(); + let meta = meta::MetaInfo::new(); if !check_meta(&meta) { return; } let config = AppConfig::new(meta.clone()).await; - let pool = db::init_db(&config, false).await.unwrap(); match meta.cmd { meta::Commands::Update => { - if db::update(pool).await { + if elerp_service::update(config).await { info!("Data updated!") } else { warn!("Nothing to update!") } } meta::Commands::Serve => { - if db::update(pool.clone()).await { - info!("Data updated!") - } info!("Elerp starting.."); - services::serve(config, pool).await + elerp_service::serve(config).await } } } -fn check_meta(meta: &MetaInfo) -> bool { +fn check_meta(meta: &meta::MetaInfo) -> bool { if !meta.data_path.is_dir() { warn!("`data-path is not directory or not found!`"); false diff --git a/server/src/myhelper.rs b/server/src/myhelper.rs deleted file mode 100644 index 5652b43..0000000 --- a/server/src/myhelper.rs +++ /dev/null @@ -1,9 +0,0 @@ -use ahash::HashSet; - -pub fn i64_safe_max() -> i64 { - i64::MAX - 1 -} - -pub fn set_to_string(set: &HashSet, sep: &str) -> String { - set.iter().map(|n|n.to_string()).collect::>().join(sep) -} \ No newline at end of file diff --git a/server/tests/common/mod.rs b/server/tests/common/mod.rs new file mode 100644 index 0000000..12b46c2 --- /dev/null +++ b/server/tests/common/mod.rs @@ -0,0 +1,313 @@ +use area_module::AreaModule; +use elerp_common::{ + area_module::model::area::Area, + get_test_config, + order_category_module::model::order_category::OrderCategory, + person_module::model::person::Person, + sku_category_module::model::sku_category::SKUCategory, + sku_module::model::sku::SKU, + user_system::model::user_info::{UserInfo, UserType}, + warehouse_module::model::warehouse::Warehouse, +}; +use guest_order_module::GuestOrderModule; +use inventory_module::InventoryModule; +use order_category_module::OrderCategoryModule; +use order_module::OrderModule; +use order_payment_module::OrderPaymentModule; +use person_module::PersonModule; +use public_system::PublicSystem; +use sku_category_module::SKUCategoryModule; +use sku_module::SKUModule; +use user_system::UserSystem; +use warehouse_module::WarehouseModule; + +pub struct TestContext { + pub ps: PublicSystem, + pub us: UserSystem, + pub area: AreaModule, + pub person: PersonModule, + pub warehouse: WarehouseModule, + pub sku: SKUModule, + pub sku_category: SKUCategoryModule, + pub order: OrderModule, + pub order_category: OrderCategoryModule, + pub order_payment: OrderPaymentModule, + pub guest_order: GuestOrderModule, + pub inventory: InventoryModule, +} + +pub struct TestPrelude { + pub user1: UserInfo, + pub user2: UserInfo, + pub area1: Area, + pub area2: Area, + pub person1: Person, + pub person2: Person, + pub warehouse1: Warehouse, + pub warehouse2: Warehouse, + pub sku_category1: SKUCategory, + pub sku_category2: SKUCategory, + pub sku1: SKU, + pub sku2: SKU, + pub order_category1: OrderCategory, + pub order_category2: OrderCategory, +} + +pub async fn init_ctx() -> TestContext { + let config = get_test_config(); + let ps = PublicSystem::new(config.clone()).await; + + TestContext { + us: UserSystem::new(ps.clone()).await, + area: AreaModule::new(ps.clone()).await, + person: PersonModule::new(ps.clone()).await, + warehouse: WarehouseModule::new(ps.clone()).await, + sku: SKUModule::new(ps.clone()).await, + sku_category: SKUCategoryModule::new(ps.clone()).await, + order: OrderModule::new(ps.clone()).await, + order_category: OrderCategoryModule::new(ps.clone()).await, + order_payment: OrderPaymentModule::new(ps.clone()).await, + guest_order: GuestOrderModule::new(ps.clone()).await, + inventory: InventoryModule::new(ps.clone()).await, + ps, + } +} + +pub async fn prelude(ctx: &TestContext) -> TestPrelude { + let mut tx = ctx.ps.begin_tx(true).await.unwrap(); + + let area1 = ctx + .area + .add( + Area { + id: 0, + name: "Area 1".to_owned(), + description: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let area2 = ctx + .area + .add( + Area { + id: 0, + name: "Area 2".to_owned(), + description: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let person1 = ctx + .person + .add( + Person { + id: 0, + name: "Person 1".to_owned(), + description: "".to_owned(), + address: "".to_owned(), + area_id: area1.id, + person_in_charge_id: 0, + contact: "".to_owned(), + email: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let person2 = ctx + .person + .add( + Person { + id: 0, + name: "Person 2".to_owned(), + description: "person in charge is person 1.".to_owned(), + address: "".to_owned(), + area_id: area2.id, + person_in_charge_id: person1.id, + contact: "".to_owned(), + email: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let warehouse1 = ctx + .warehouse + .add( + Warehouse { + id: 0, + name: "Warehouse 1".to_owned(), + description: "".to_owned(), + person_in_charge_id: person1.id, + area_id: area1.id, + address: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let warehouse2 = ctx + .warehouse + .add( + Warehouse { + id: 0, + name: "Warehouse 2".to_owned(), + description: "".to_owned(), + person_in_charge_id: person2.id, + area_id: area2.id, + address: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let sku_category1 = ctx + .sku_category + .add( + SKUCategory { + id: 0, + name: "SKU Category 1".to_owned(), + description: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let sku_category2 = ctx + .sku_category + .add( + SKUCategory { + id: 0, + name: "SKU Category 2".to_owned(), + description: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let sku1 = ctx + .sku + .add( + SKU { + id: 0, + sku_category_id: sku_category1.id, + name: "SKU 1".to_owned(), + description: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let sku2 = ctx + .sku + .add( + SKU { + id: 0, + sku_category_id: sku_category1.id, + name: "SKU 2".to_owned(), + description: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let order_category1 = ctx + .order_category + .add( + OrderCategory { + id: 0, + name: "Order Category 1".to_owned(), + description: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + let order_category2 = ctx + .order_category + .add( + OrderCategory { + id: 0, + name: "Order Category 2".to_owned(), + description: "".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + + let prelude = TestPrelude { + user1: ctx + .us + .add_user( + UserInfo { + id: 0, + alias: "User 1".to_owned(), + username: "user1".to_owned(), + password: "tester".to_owned(), + user_type: UserType::General, + permission: 0, + }, + tx.as_mut(), + ) + .await + .unwrap(), + user2: ctx + .us + .add_user( + UserInfo { + id: 0, + alias: "User 2".to_owned(), + username: "user2".to_owned(), + password: "tester".to_owned(), + user_type: UserType::General, + permission: 0, + }, + tx.as_mut(), + ) + .await + .unwrap(), + area1, + area2, + person1, + person2, + warehouse1, + warehouse2, + sku_category1, + sku_category2, + sku1, + sku2, + order_category1, + order_category2, + }; + + tx.commit().await.unwrap(); + prelude +} diff --git a/server/tests/test_area_module.rs b/server/tests/test_area_module.rs new file mode 100644 index 0000000..25716b7 --- /dev/null +++ b/server/tests/test_area_module.rs @@ -0,0 +1,46 @@ +pub mod common; + +use elerp_common::area_module::model::area::{Area, GetAreasQuery}; + +#[tokio::test] +async fn test_module() { + let c = common::init_ctx().await; + + let mut tx = c.ps.begin_tx(false).await.unwrap(); + assert_eq!(c.area.is_limit_reached(tx.as_mut()).await.unwrap(), false); + tx.commit().await.unwrap(); + + let max = c.ps.get_config().limit.areas; + let mut last_name = "".to_owned(); + let mut last_id = 0; + for n in 0..max { + let mut tx = c.ps.begin_tx(true).await.unwrap(); + let r = c + .area + .add( + Area { + id: 0, + name: format!("Test area #{n}"), + description: "testing area".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + tx.commit().await.unwrap(); + last_name = r.name; + last_id = r.id; + } + let mut tx = c.ps.begin_tx(false).await.unwrap(); + let count = c.area.get_count(&GetAreasQuery::default(), tx.as_mut()).await.unwrap(); + assert_eq!(count, max); + assert_eq!(c.area.is_limit_reached(tx.as_mut()).await.unwrap(), true); + assert_eq!(c.area.is_exists(last_id, tx.as_mut()).await.unwrap(), true); + assert_eq!(c.area.is_exists_name(&last_name, None, tx.as_mut()).await.unwrap(), true); + assert_eq!(c.area.is_exists_name(&last_name, Some(last_id), tx.as_mut()).await.unwrap(), false); + + let row = c.area.get(last_id, tx.as_mut()).await.unwrap(); + assert!(row.is_some()) +} diff --git a/server/tests/test_guest_order.rs b/server/tests/test_guest_order.rs new file mode 100644 index 0000000..d2381e1 --- /dev/null +++ b/server/tests/test_guest_order.rs @@ -0,0 +1,98 @@ +mod common; + +use elerp_common::{ + guest_order_module::model::guest_order::{GuestOrder, GuestOrderStatus}, + order_module::model::order::{OrderCurrency, OrderItem, OrderType}, +}; + +#[tokio::test] +async fn test_order_preprocess() { + let c = common::init_ctx().await; + let p = common::prelude(&c).await; + + let mut guest = GuestOrder { + id: 0, + created_by_user_id: 0, + person_in_charge_id: 0, + date: 0, + confirmed_date: 0, + sub_token: "testing".to_owned(), + currency: OrderCurrency::USD, + warehouse_id: p.warehouse1.id, + person_related_id: p.person2.id, + description: "".to_owned(), + order_type: OrderType::StockOut, + guest_order_status: GuestOrderStatus::Expired, + order_id: 0, + order_category_id: p.order_category1.id, + items: Some(vec![]), + }; + c.guest_order.preprocess(&mut guest, &p.user1, p.person1.id); + assert_eq!(guest.created_by_user_id, p.user1.id); + assert_eq!(guest.person_in_charge_id, p.person1.id); + assert!(guest.items.is_none()) +} + +#[tokio::test] +async fn test_module() { + let c = common::init_ctx().await; + let p = common::prelude(&c).await; + + let max = c.ps.get_config().limit.guest_orders; + + // Stock in orders. + for n in 0..max { + let mut tx = c.ps.begin_tx(true).await.unwrap(); + + let mut guest = GuestOrder { + id: 0, + created_by_user_id: 0, + person_in_charge_id: 0, + date: 0, + confirmed_date: 0, + sub_token: format!("testing #{n}"), + currency: OrderCurrency::USD, + warehouse_id: p.warehouse1.id, + person_related_id: p.person2.id, + description: "".to_owned(), + order_type: OrderType::StockOut, + guest_order_status: GuestOrderStatus::Expired, + order_id: 0, + order_category_id: p.order_category1.id, + items: Some(vec![OrderItem { + sku_id: p.sku1.id, + quantity: 5, + price: 1.0, + exchanged: false, + }]), + }; + let mut to_confirm = guest.clone(); + c.guest_order.preprocess(&mut guest, &p.user1, p.person1.id); + + let r = c.guest_order.add("testing_changed", guest, tx.as_mut()).await.unwrap(); + assert!(r.date > 0); + assert!(r.confirmed_date > 0); + assert_eq!(r.sub_token, "testing_changed"); + + let r = c.guest_order.add("testing_changed", to_confirm.clone(), tx.as_mut()).await.unwrap(); + let result = c.guest_order.confirm(r.id, to_confirm.clone(), tx.as_mut()).await.unwrap().unwrap(); + assert!(!result.check_result.items_not_available.is_empty()); + assert!(result.order.is_none()); + + to_confirm.order_type = OrderType::Calibration; + let r = c.guest_order.add("testing_changed", to_confirm.clone(), tx.as_mut()).await.unwrap(); + let result = c.guest_order.confirm(r.id, to_confirm.clone(), tx.as_mut()).await.unwrap().unwrap(); + assert!(result.check_result.items_not_available.is_empty()); + assert_eq!(c.inventory.get(to_confirm.warehouse_id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 5); + assert!(result.order.is_some()); + + to_confirm.order_type = OrderType::StockOut; + to_confirm.items.as_mut().map(|items| items[0].quantity = 3); + let r = c.guest_order.add("testing_changed", to_confirm.clone(), tx.as_mut()).await.unwrap(); + let result = c.guest_order.confirm(r.id, to_confirm.clone(), tx.as_mut()).await.unwrap().unwrap(); + assert!(result.check_result.items_not_available.is_empty()); + assert_eq!(c.inventory.get(to_confirm.warehouse_id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 2); + assert!(result.order.is_some()); + tx.commit().await.unwrap(); + } +} diff --git a/server/tests/test_order_module.rs b/server/tests/test_order_module.rs new file mode 100644 index 0000000..d0481ad --- /dev/null +++ b/server/tests/test_order_module.rs @@ -0,0 +1,344 @@ +mod common; + +use elerp_common::{ + model::action_type::ActionType, + order_module::model::order::{GetOrdersQuery, Order, OrderCurrency, OrderItem, OrderPaymentStatus, OrderType}, + sql, +}; + +#[tokio::test] +async fn test_order_preprocess() { + let c = common::init_ctx().await; + let p = common::prelude(&c).await; + + let mut order = Order { + id: 0, + created_by_user_id: 0, + updated_by_user_id: 0, + date: 0, + last_updated_date: 0, + person_in_charge_id: 0, + order_category_id: p.order_category1.id, + from_guest_order_id: 0, + currency: OrderCurrency::USD, + items: Some(vec![ + OrderItem { + sku_id: p.sku1.id, + quantity: 100, + price: 18.5, + exchanged: true, + }, + OrderItem { + sku_id: p.sku2.id, + quantity: 250, + price: 10.0, + exchanged: false, + }, + ]), + total_amount: 0.0, + total_amount_settled: 0.0, + order_payment_status: OrderPaymentStatus::None, + warehouse_id: p.warehouse1.id, + person_related_id: p.person1.id, + description: format!("Testing order #1"), + order_type: OrderType::StockIn, + }; + c.order.preprocess(&mut order, &p.user1, true, p.person2.id); + assert_eq!(order.created_by_user_id, p.user1.id); + assert_eq!(order.updated_by_user_id, p.user1.id); + assert_eq!(order.person_in_charge_id, p.person2.id); + assert_eq!(order.from_guest_order_id, 0); + assert!(order.date > 0); + assert_eq!(order.items.as_ref().unwrap().len(), 1); + assert_eq!(order.items.as_ref().unwrap()[0].sku_id, p.sku2.id); + assert_eq!(order.items.as_ref().unwrap()[0].quantity, 250); + assert_eq!(order.items.as_ref().unwrap()[0].price, 10.0); + assert_eq!(order.items.as_ref().unwrap()[0].exchanged, false); +} + +#[tokio::test] +async fn test_module() { + let c = common::init_ctx().await; + let p = common::prelude(&c).await; + + let max = c.ps.get_config().limit.orders; + + // Stock in orders. + for n in 0..max { + let mut tx = c.ps.begin_tx(true).await.unwrap(); + + let mut order = Order { + id: 0, + created_by_user_id: 0, + updated_by_user_id: 0, + date: 0, + last_updated_date: 0, + person_in_charge_id: 0, + order_category_id: p.order_category1.id, + from_guest_order_id: 0, + currency: OrderCurrency::USD, + items: Some(vec![ + OrderItem { + sku_id: p.sku1.id, + quantity: 100, + price: 18.5, + exchanged: false, + }, + OrderItem { + sku_id: p.sku2.id, + quantity: 250, + price: 10.0, + exchanged: false, + }, + ]), + total_amount: 0.0, + total_amount_settled: 0.0, + order_payment_status: OrderPaymentStatus::None, + warehouse_id: p.warehouse1.id, + person_related_id: p.person1.id, + description: format!("Testing order #{n}"), + order_type: OrderType::StockIn, + }; + c.order.preprocess(&mut order, &p.user1, true, p.person1.person_in_charge_id); + + let r = c.order.add(order, tx.as_mut()).await.unwrap(); + + assert_eq!(r.total_amount, 4350.0); + assert_eq!(r.total_amount_settled, 0.0); + + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 100 * (n + 1)); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 250 * (n + 1)); + + assert_eq!(c.order.is_limit_reached(tx.as_mut()).await.unwrap(), if n == max - 1 { true } else { false }); + assert!(c.order.is_exists(r.id, tx.as_mut()).await.unwrap()); + assert!(!c.order.is_exists(r.id + 1, tx.as_mut()).await.unwrap()); + + tx.commit().await.unwrap(); + } + + let mut tx = c.ps.begin_tx(false).await.unwrap(); + let count = c.order.get_count(&GetOrdersQuery::default(), ActionType::System, tx.as_mut()).await.unwrap(); + assert_eq!(count, max); + + let sku1_qty = 100 * max; + let sku2_qty = 250 * max; + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku1_qty); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku2_qty); + tx.commit().await.unwrap(); + + // Test other type orders. + let mut tx = c.ps.begin_tx(true).await.unwrap(); + let mut stock_out_order = Order { + id: 0, + created_by_user_id: 0, + updated_by_user_id: 0, + date: 0, + last_updated_date: 0, + person_in_charge_id: 0, + order_category_id: p.order_category1.id, + from_guest_order_id: 0, + currency: OrderCurrency::USD, + items: Some(vec![ + OrderItem { + sku_id: p.sku1.id, + quantity: 50 * max, + price: 1.0, + exchanged: false, + }, + OrderItem { + sku_id: p.sku2.id, + quantity: 125 * max, + price: 5.0, + exchanged: false, + }, + ]), + total_amount: 0.0, + total_amount_settled: 0.0, + order_payment_status: OrderPaymentStatus::None, + warehouse_id: p.warehouse1.id, + person_related_id: p.person1.id, + description: format!("Testing stock out..."), + order_type: OrderType::StockOut, + }; + c.order.preprocess(&mut stock_out_order, &p.user1, true, p.person1.person_in_charge_id); + let stock_out_order = c.order.add(stock_out_order, tx.as_mut()).await.unwrap(); + assert_eq!(stock_out_order.order_payment_status, OrderPaymentStatus::Unsettled); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku1_qty / 2); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku2_qty / 2); + + c.order.remove(stock_out_order.id, true, false, ActionType::System, tx.as_mut()).await.unwrap(); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku1_qty); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku2_qty); + + let mut exchange_order = Order { + id: 0, + created_by_user_id: 0, + updated_by_user_id: 0, + date: 0, + last_updated_date: 0, + person_in_charge_id: 0, + order_category_id: p.order_category1.id, + from_guest_order_id: 0, + currency: OrderCurrency::USD, + items: Some(vec![ + OrderItem { + sku_id: p.sku1.id, + quantity: 50 * max, + price: 0.0, + exchanged: false, + }, + OrderItem { + sku_id: p.sku2.id, + quantity: 125 * max, + price: 0.0, + exchanged: true, + }, + ]), + total_amount: 0.0, + total_amount_settled: 0.0, + order_payment_status: OrderPaymentStatus::None, + warehouse_id: p.warehouse1.id, + person_related_id: p.person1.id, + description: format!("Testing stock out..."), + order_type: OrderType::Exchange, + }; + c.order.preprocess(&mut exchange_order, &p.user1, true, p.person1.person_in_charge_id); + let exchange_order = c.order.add(exchange_order, tx.as_mut()).await.unwrap(); + assert_eq!(exchange_order.order_payment_status, OrderPaymentStatus::None); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku1_qty / 2); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 125 * max + sku2_qty); + + c.order.remove(exchange_order.id, true, false, ActionType::System, tx.as_mut()).await.unwrap(); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku1_qty); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, sku2_qty); + + let mut calibration_order = Order { + id: 0, + created_by_user_id: 0, + updated_by_user_id: 0, + date: 0, + last_updated_date: 0, + person_in_charge_id: 0, + order_category_id: p.order_category1.id, + from_guest_order_id: 0, + currency: OrderCurrency::USD, + items: Some(vec![ + OrderItem { + sku_id: p.sku1.id, + quantity: 999, + price: 0.0, + exchanged: false, + }, + OrderItem { + sku_id: p.sku2.id, + quantity: 666, + price: 0.0, + exchanged: false, + }, + ]), + total_amount: 0.0, + total_amount_settled: 0.0, + order_payment_status: OrderPaymentStatus::None, + warehouse_id: p.warehouse1.id, + person_related_id: p.person1.id, + description: format!("Testing stock out..."), + order_type: OrderType::Calibration, + }; + c.order.preprocess(&mut calibration_order, &p.user1, true, p.person1.person_in_charge_id); + let calibration_order = c.order.add(calibration_order, tx.as_mut()).await.unwrap(); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 999); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 666); + + let mut stock_in_order = Order { + id: 0, + created_by_user_id: 0, + updated_by_user_id: 0, + date: 0, + last_updated_date: 0, + person_in_charge_id: 0, + order_category_id: p.order_category1.id, + from_guest_order_id: 0, + currency: OrderCurrency::USD, + items: Some(vec![ + OrderItem { + sku_id: p.sku1.id, + quantity: 1, + price: 0.0, + exchanged: false, + }, + OrderItem { + sku_id: p.sku2.id, + quantity: 334, + price: 0.0, + exchanged: false, + }, + ]), + total_amount: 0.0, + total_amount_settled: 0.0, + order_payment_status: OrderPaymentStatus::None, + warehouse_id: p.warehouse1.id, + person_related_id: p.person1.id, + description: format!("Testing stock out..."), + order_type: OrderType::StockIn, + }; + c.order.preprocess(&mut stock_in_order, &p.user1, true, p.person1.person_in_charge_id); + let stock_in_order = c.order.add(stock_in_order, tx.as_mut()).await.unwrap(); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 1000); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 1000); + + c.order.remove(calibration_order.id, true, false, ActionType::System, tx.as_mut()).await.unwrap(); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 100 * max + 1); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 250 * max + 334); + + c.order.remove(stock_in_order.id, true, false, ActionType::System, tx.as_mut()).await.unwrap(); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 100 * max); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 250 * max); + + let mut calibration_strict_order = Order { + id: 0, + created_by_user_id: 0, + updated_by_user_id: 0, + date: 0, + last_updated_date: 0, + person_in_charge_id: 0, + order_category_id: p.order_category1.id, + from_guest_order_id: 0, + currency: OrderCurrency::USD, + items: Some(vec![OrderItem { + sku_id: p.sku1.id, + quantity: 1314, + price: 0.0, + exchanged: false, + }]), + total_amount: 0.0, + total_amount_settled: 0.0, + order_payment_status: OrderPaymentStatus::None, + warehouse_id: p.warehouse1.id, + person_related_id: p.person1.id, + description: format!("Testing stock out..."), + order_type: OrderType::CalibrationStrict, + }; + c.order.preprocess(&mut calibration_strict_order, &p.user1, true, p.person1.person_in_charge_id); + let calibration_strict_order = c.order.add(calibration_strict_order, tx.as_mut()).await.unwrap(); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 1314); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 0); + + c.order.remove(calibration_strict_order.id, true, false, ActionType::System, tx.as_mut()).await.unwrap(); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 100 * max); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 250 * max); + + tx.commit().await.unwrap(); + + let mut tx = c.ps.begin_tx(true).await.unwrap(); + for n in 0..max { + let id = sql::get_standard_id(n + 1); + assert!(c.order.is_exists(id, tx.as_mut()).await.unwrap()); + c.order.remove(id, true, false, ActionType::System, tx.as_mut()).await.unwrap(); + assert!(!c.order.is_exists(id, tx.as_mut()).await.unwrap()); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 100 * max - 100 * (n + 1)); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 250 * max - 250 * (n + 1)); + } + + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku1.id, tx.as_mut()).await.unwrap().unwrap().quantity, 0); + assert_eq!(c.inventory.get(p.warehouse1.id, p.sku2.id, tx.as_mut()).await.unwrap().unwrap().quantity, 0); +} diff --git a/server/tests/test_order_payment_module.rs b/server/tests/test_order_payment_module.rs new file mode 100644 index 0000000..fcbd4b2 --- /dev/null +++ b/server/tests/test_order_payment_module.rs @@ -0,0 +1,113 @@ +use elerp_common::{ + order_module::model::order::{Order, OrderCurrency, OrderItem, OrderPaymentStatus, OrderType}, + order_payment_module::model::order_payment::OrderPayment, + sql, +}; + +mod common; + +#[tokio::test] +async fn test_preprocess() { + let c = common::init_ctx().await; + let p = common::prelude(&c).await; + + let mut payment = OrderPayment { + id: 0, + created_by_user_id: 0, + order_id: 0, + warehouse_id: 0, + person_in_charge_id: 0, + creation_date: 0, + actual_date: 0, + total_amount: 0.0, + remark: "HelloWorld".to_owned(), + }; + c.order_payment.preprocess(&mut payment, &p.user1); + assert!(payment.creation_date > 0); + assert_eq!(payment.created_by_user_id, p.user1.id); +} + +#[tokio::test] +async fn test_module() { + let c = common::init_ctx().await; + let p = common::prelude(&c).await; + + let mut tx = c.ps.begin_tx(true).await.unwrap(); + let mut order = Order { + id: 0, + created_by_user_id: 0, + updated_by_user_id: 0, + date: 0, + last_updated_date: 0, + person_in_charge_id: 0, + order_category_id: p.order_category1.id, + from_guest_order_id: 0, + currency: OrderCurrency::USD, + items: Some(vec![OrderItem { + sku_id: p.sku1.id, + quantity: 1314, + price: 1.0, + exchanged: false, + }]), + total_amount: 0.0, + total_amount_settled: 0.0, + order_payment_status: OrderPaymentStatus::None, + warehouse_id: p.warehouse1.id, + person_related_id: p.person1.id, + description: format!("Testing stock out..."), + order_type: OrderType::StockOut, + }; + c.order.preprocess(&mut order, &p.user1, true, p.person1.person_in_charge_id); + let order = c.order.add(order, tx.as_mut()).await.unwrap(); + + let mut payment = OrderPayment { + id: 0, + created_by_user_id: 0, + order_id: order.id, + warehouse_id: 0, + person_in_charge_id: 0, + creation_date: 0, + actual_date: 0, + total_amount: 314.0, + remark: "HelloWorld".to_owned(), + }; + c.order_payment.preprocess(&mut payment, &p.user1); + let payment = c.order_payment.add(payment, tx.as_mut()).await.unwrap(); + assert_eq!(payment.id, sql::get_standard_id(1)); + assert!(payment.creation_date > 0); + assert_eq!(payment.warehouse_id, order.warehouse_id); + + let order = c.order.get(order.id, tx.as_mut()).await.unwrap().unwrap(); + assert_eq!(order.total_amount_settled, payment.total_amount); + assert_eq!(order.order_payment_status, OrderPaymentStatus::PartialSettled); + + let mut payment2 = OrderPayment { + id: 0, + created_by_user_id: 0, + order_id: order.id, + warehouse_id: 0, + person_in_charge_id: 0, + creation_date: 0, + actual_date: 0, + total_amount: 1001.0, + remark: "HelloWorld".to_owned(), + }; + c.order_payment.preprocess(&mut payment2, &p.user1); + let payment2 = c.order_payment.add(payment2, tx.as_mut()).await.unwrap(); + assert_eq!(payment2.id, sql::get_standard_id(2)); + assert!(payment2.creation_date > 0); + + let order = c.order.get(order.id, tx.as_mut()).await.unwrap().unwrap(); + assert_eq!(order.total_amount_settled, payment.total_amount + payment2.total_amount); + assert_eq!(order.order_payment_status, OrderPaymentStatus::Settled); + + c.order_payment.remove(payment.id, false, tx.as_mut()).await.unwrap(); + let order = c.order.get(order.id, tx.as_mut()).await.unwrap().unwrap(); + assert_eq!(order.total_amount_settled, payment2.total_amount); + assert_eq!(order.order_payment_status, OrderPaymentStatus::PartialSettled); + + c.order_payment.remove(payment2.id, false, tx.as_mut()).await.unwrap(); + let order = c.order.get(order.id, tx.as_mut()).await.unwrap().unwrap(); + assert_eq!(order.total_amount_settled, 0.0); + assert_eq!(order.order_payment_status, OrderPaymentStatus::Unsettled); +} diff --git a/server/tests/test_person_module.rs b/server/tests/test_person_module.rs new file mode 100644 index 0000000..27e9c95 --- /dev/null +++ b/server/tests/test_person_module.rs @@ -0,0 +1,86 @@ +use area_module::AreaModule; +use elerp_common::{ + area_module::model::area::Area, + get_test_config, + model::action_type::ActionType, + person_module::model::person::{GetPersonsQuery, Person}, +}; +use person_module::PersonModule; +use public_system::PublicSystem; + +#[tokio::test] +async fn test_module() { + let config = get_test_config(); + let ps = PublicSystem::new(config.clone()).await; + let m = PersonModule::new(ps.clone()).await; + let area_m = AreaModule::new(ps.clone()).await; + let mut tx = ps.begin_tx(true).await.unwrap(); + let area = area_m + .add( + Area { + id: 0, + name: "Test area for persons".to_owned(), + description: "testing".to_owned(), + color: None, + text_color: None, + }, + tx.as_mut(), + ) + .await + .unwrap(); + assert_eq!(m.is_limit_reached(tx.as_mut()).await.unwrap(), false); + tx.commit().await.unwrap(); + + let max = config.limit.persons; + let mut last_name = "".to_owned(); + let mut last_id = 0; + for n in 0..max { + let mut tx = ps.begin_tx(true).await.unwrap(); + let r = m + .add( + Person { + id: 0, + name: format!("Test area #{n}"), + description: "testing area".to_owned(), + color: None, + text_color: None, + address: "address...".to_owned(), + area_id: area.id, + person_in_charge_id: 0, + contact: "0123456789".to_owned(), + email: "example@email.com".to_owned(), + }, + tx.as_mut(), + ) + .await + .unwrap(); + tx.commit().await.unwrap(); + last_name = r.name; + last_id = r.id; + } + let mut tx = ps.begin_tx(false).await.unwrap(); + let mut query = GetPersonsQuery::default(); + query.area_id = Some(area.id); + let count = m.get_count(&query, tx.as_mut()).await.unwrap(); + assert_eq!(count, max); + assert_eq!(m.is_limit_reached(tx.as_mut()).await.unwrap(), true); + assert_eq!(m.is_exists(last_id, tx.as_mut()).await.unwrap(), true); + assert_eq!( + m.is_exists_name(&last_name, None, tx.as_mut()) + .await + .unwrap(), + true + ); + assert_eq!( + m.is_exists_name(&last_name, Some(last_id), tx.as_mut()) + .await + .unwrap(), + false + ); + + let row = m + .get(last_id, ActionType::System, tx.as_mut()) + .await + .unwrap(); + assert!(row.is_some()) +} diff --git a/web/src/components/SmartForm/modal/AddOrUpdate/Modal.vue b/web/src/components/SmartForm/modal/AddOrUpdate/Modal.vue index cfdbcf8..e9d8acf 100644 --- a/web/src/components/SmartForm/modal/AddOrUpdate/Modal.vue +++ b/web/src/components/SmartForm/modal/AddOrUpdate/Modal.vue @@ -101,6 +101,7 @@ defineExpose({ const updatedSet = ref(new Set()); watch(template, () => updatedSet.value.clear()); function checkUpdate(key: string) { + console.log('hello') if (!isEqual(template.value[key], mutTemplate.value[key])) { updatedSet.value.add(key); } else { @@ -243,7 +244,7 @@ function checkUpdate(key: string) { (); const emit = defineEmits<{ (e: "update:value", v: UserPermission): void; + (e: "change", v: UserPermission): void; }>(); const permissions = [ @@ -58,14 +59,11 @@ const permissions = [