diff --git a/Cargo.lock b/Cargo.lock index 4804902..8838a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,18 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -623,6 +611,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -814,27 +808,22 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" - [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown", ] [[package]] @@ -961,7 +950,7 @@ dependencies = [ "reqwest_cookie_store", "serde", "serde_json", - "thiserror", + "thiserror 1.0.65", "tokio", ] @@ -1141,7 +1130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown", ] [[package]] @@ -1162,6 +1151,7 @@ dependencies = [ "reqwest 0.12.9", "ron", "serde", + "serde_json", "sqlx", "tokio", "tokio-stream", @@ -1446,7 +1436,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "sha2", - "thiserror", + "thiserror 1.0.65", "url", ] @@ -2209,21 +2199,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2234,38 +2214,33 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ - "atoi", - "byteorder", + "base64 0.22.1", "bytes", "chrono", "crc", "crossbeam-queue", "either", "event-listener", - "futures-channel", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.14.5", + "hashbrown", "hashlink", - "hex", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", "serde", "serde_json", "sha2", "smallvec", - "sqlformat", - "thiserror", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -2275,9 +2250,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", @@ -2288,9 +2263,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", @@ -2307,16 +2282,15 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn", - "tempfile", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", @@ -2350,7 +2324,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.12", "time", "tracing", "whoami", @@ -2358,9 +2332,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", @@ -2372,7 +2346,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf", @@ -2390,7 +2363,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.12", "time", "tracing", "whoami", @@ -2398,9 +2371,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.2" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -2416,6 +2389,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", + "thiserror 2.0.12", "time", "tracing", "url", @@ -2440,9 +2414,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.85" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -2525,7 +2499,16 @@ version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.65", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -2539,6 +2522,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -2793,7 +2787,7 @@ dependencies = [ "rand", "serde", "serde_json", - "thiserror", + "thiserror 1.0.65", "time", "tokio", "tracing", @@ -2820,7 +2814,7 @@ dependencies = [ "async-trait", "rmp-serde", "sqlx", - "thiserror", + "thiserror 1.0.65", "time", "tower-sessions-core", ] @@ -2932,12 +2926,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 52c317b..923b911 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ axum = { version = "0.7.7", features = ["macros", "multipart"] } axum-htmx = { version = "0.6.0", features = ["auto-vary"] } dotenvy = "0.15.7" oauth2 = "4.4.2" -sqlx = { version = "0.8.2", default-features = false, features = ["runtime-tokio", "sqlite", "chrono", "macros"] } +sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio", "sqlite", "chrono", "macros"] } tokio = { version = "1.41.0", features = ["full", "tracing"] } tower = { version = "0.5.1", features = ["util"] } tower-http = { version = "0.6.1", features = ["fs", "trace"] } @@ -19,6 +19,7 @@ tower-sessions-sqlx-store = { version = "0.14.1", features = ["sqlite"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } serde = { version = "1.0.213", features = ["derive"] } +serde_json = "1.0" reqwest = { version = "0.12.9", features = ["json"] } askama_axum = "0.4.0" axum-extra = "0.9.4" diff --git a/data/initial_catalog.csv b/data/initial_catalog.csv index 88f0862..4ccf5e1 100644 --- a/data/initial_catalog.csv +++ b/data/initial_catalog.csv @@ -1,59 +1,59 @@ -name,qty,unit,fractional,reorder,price -Amoxicillin/Clavulanate 62.5mg/ml 15ml,1,ct,false,10,25 -Animax Ointment 15ml,1,ct,false,10,25 -Buprenex 0.3mg/ml - Injectable,1,ct,false,10,25 -Buprenex 0.3mg/ml Oral Solution,28,ct,false,10,25 -Carprofen 100mg,168,ct,false,10,25 -Carprofen 25mg,232,ct,false,10,25 -Carprofen 75mg,80,ct,false,10,25 -Cephalexin 250mg,605,ct,false,10,25 -Cephalexin 500mg,513,ct,false,10,25 -Cerenia 10mg/ml (Per ml),12,ml,true,10,25 -Cough Tabs,325,ct,false,10,25 -Cytopoint 10mg,2,ct,false,10,25 -Cytopoint 20mg,0,ct,false,10,25 -Cytopoint 30mg,4,ct,false,10,25 -Cytopoint 40mg,6,ct,false,10,25 -Dexamethasone 2mg/ml Injectable,99,ct,false,10,25 -Dog Ends Starter Kit,1,ct,false,10,25 -Doxycycline 100mg Tablet,104,ct,false,10,25 -Elura 20mg/ml - 15ml,1,ct,false,10,25 -Entyce 30mg/ml - 10ml bottle,1,ct,false,10,25 -Fenbendazole 100mg/ml,900,ct,false,10,25 -"Florfenicol, Terbinafine, Mometasone furoate Ear Treament 1ml Tube",68,ct,false,10,25 -FortiFlora 30ct - Cat,2,ct,false,10,25 -FortiFlora SA K9,3,ct,false,10,25 -FVRCP TruFel Ultra Injectable,9,ct,false,10,25 -Gabapentin 100mg,479,ct,false,10,25 -Gabapentin 600mg Tablets,503,ct,false,10,25 -Gentamicin Sulfate/Betamethasone Spray 60ml,0,ct,false,10,25 -Kenalog 10mg/ml (Per ml),8.52,ml,true,10,25 -Librela 10mg/ml,1,ct,false,10,25 -Librela 20mg/ml,20,ct,false,10,25 -Librela 30mg/ml,25,ct,false,10,25 -Librela 5mg/ml,3,ct,false,10,25 -Maropitant 16mg,10,ct,false,10,25 -Maropitant 24mg,8,ct,false,10,25 -Maropitant 60mg,16,ct,false,10,25 -Meloxicam 1.5mg/ml - 10ml,2,ct,false,10,25 -Metronidazole 125mg/ml - 30ML,0,ct,false,10,25 -Metronidazole 250mg,15,ct,false,10,25 -Mirataz 5g,3,ct,false,10,25 -NeoPolyBac Ointment,1,ct,false,10,25 -Nobivac DAPP,64,ct,false,10,25 -Nobivac Intra-Trac Bordetella,68,ct,false,10,25 -Nobivac Lepto 4,42,ct,false,10,25 -Nobivac Rabies,96,ct,false,10,25 -Ondansetron 8mg,16,ct,false,10,25 -Praziquantel 56.8 mg/ml Injection,8,ct,false,10,25 -Prednisolone 5mg,913,ct,false,10,25 -Prednisone 20mg,259,ct,false,10,25 -Pro-Pectalin 15ml,2,ct,false,10,25 -Probiotic Powder Canine - 30ct,1,ct,false,10,25 -PureVax Feline Rabies,25,ct,false,10,25 -PureVax Recombinant FeLV,13,ct,false,10,25 -Rilexine 150mg,12,ct,false,10,25 -Rilexine 300mg,34,ct,false,10,25 -Tobramycin Ophthalmic Sol 5ml,3,ct,false,10,25 -Trazodone 100mg,257,ct,false,10,25 -Varenzin-CA1 25mg/ml,2,ct,false,10,25 +name,qty,unit,fractional,reorder,price,vetcove id +Amoxicillin/Clavulanate 62.5mg/ml 15ml,1,ct,false,10,25,628688 +Animax Ointment 15ml,1,ct,false,10,25,143513 +Buprenex 0.3mg/ml - Injectable,1,ct,false,10,25, +Buprenex 0.3mg/ml Oral Solution,28,ct,false,10,25, +Carprofen 100mg,168,ct,false,10,25, +Carprofen 25mg,232,ct,false,10,25, +Carprofen 75mg,80,ct,false,10,25, +Cephalexin 250mg,605,ct,false,10,25, +Cephalexin 500mg,513,ct,false,10,25,120077 +Cerenia 10mg/ml (Per ml),12,ml,true,10,25, +Cough Tabs,325,ct,false,10,25, +Cytopoint 10mg,2,ct,false,10,25, +Cytopoint 20mg,0,ct,false,10,25,298905 +Cytopoint 30mg,4,ct,false,10,25, +Cytopoint 40mg,6,ct,false,10,25,298499 +Dexamethasone 2mg/ml Injectable,99,ct,false,10,25, +Dog Ends Starter Kit,1,ct,false,10,25, +Doxycycline 100mg Tablet,104,ct,false,10,25, +Elura 20mg/ml - 15ml,1,ct,false,10,25, +Entyce 30mg/ml - 10ml bottle,1,ct,false,10,25, +Fenbendazole 100mg/ml,900,ct,false,10,25, +"Florfenicol, Terbinafine, Mometasone furoate Ear Treament 1ml Tube",68,ct,false,10,25, +FortiFlora 30ct - Cat,2,ct,false,10,25, +FortiFlora SA K9,3,ct,false,10,25, +FVRCP TruFel Ultra Injectable,9,ct,false,10,25, +Gabapentin 100mg,479,ct,false,10,25,142264 +Gabapentin 600mg Tablets,503,ct,false,10,25,11603 +Gentamicin Sulfate/Betamethasone Spray 60ml,0,ct,false,10,25, +Kenalog 10mg/ml (Per ml),8.52,ml,true,10,25, +Librela 10mg/ml,1,ct,false,10,25, +Librela 20mg/ml,20,ct,false,10,25,605233 +Librela 30mg/ml,25,ct,false,10,25,605231 +Librela 5mg/ml,3,ct,false,10,25, +Maropitant 16mg,10,ct,false,10,25, +Maropitant 24mg,8,ct,false,10,25, +Maropitant 60mg,16,ct,false,10,25, +Meloxicam 1.5mg/ml - 10ml,2,ct,false,10,25, +Metronidazole 125mg/ml - 30ML,0,ct,false,10,25, +Metronidazole 250mg,15,ct,false,10,25,85774 +Mirataz 5g,3,ct,false,10,25, +NeoPolyBac Ointment,1,ct,false,10,25, +Nobivac DAPP,64,ct,false,10,25, +Nobivac Intra-Trac Bordetella,68,ct,false,10,25,191649 +Nobivac Lepto 4,42,ct,false,10,25,192695 +Nobivac Rabies,96,ct,false,10,25, +Ondansetron 8mg,16,ct,false,10,25, +Praziquantel 56.8 mg/ml Injection,8,ct,false,10,25, +Prednisolone 5mg,913,ct,false,10,25, +Prednisone 20mg,259,ct,false,10,25,196593 +Pro-Pectalin 15ml,2,ct,false,10,25, +Probiotic Powder Canine - 30ct,1,ct,false,10,25, +PureVax Feline Rabies,25,ct,false,10,25,433495 +PureVax Recombinant FeLV,13,ct,false,10,25,546596 +Rilexine 150mg,12,ct,false,10,25,192751 +Rilexine 300mg,34,ct,false,10,25, +Tobramycin Ophthalmic Sol 5ml,3,ct,false,10,25, +Trazodone 100mg,257,ct,false,10,25, +Varenzin-CA1 25mg/ml,2,ct,false,10,25, diff --git a/migrations/20241107225934_initial.sql b/migrations/20241107225934_initial.sql index 0e575ee..7da0c2d 100644 --- a/migrations/20241107225934_initial.sql +++ b/migrations/20241107225934_initial.sql @@ -27,9 +27,6 @@ CREATE TABLE IF NOT EXISTS InventoryItem ( allow_fractional_units BOOLEAN NOT NULL, active BOOLEAN NOT NULL, pims_id TEXT, - vetcove_id TEXT, - manufacturer_name TEXT, - manufacturer_id TEXT, FOREIGN KEY(display_unit) REFERENCES DisplayUnit(id) ); @@ -71,136 +68,46 @@ INSERT INTO DisplayUnit (id, unit, abbreviation) VALUES (2,'milliliter', 'ml'), (3,'milligram', 'mg'); -CREATE TABLE IF NOT EXISTS SkuCovetrus +CREATE TABLE IF NOT EXISTS VetcoveItem ( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuMWI -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuPatterson -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuMidwest -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuFirstVet -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuPennVet -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuAmatheon -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuVictor -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuVetcove -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuMillerVet -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuBoehringerIngelheim -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuMillerVet -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuZoetis -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuPharmasourceAH -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuNEAnimalHealth -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuDechra -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); - -CREATE TABLE IF NOT EXISTS SkuMedline -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) + vetcove_item_id INTEGER PRIMARY KEY NOT NULL, + item_name TEXT NOT NULL, + ignored INTEGER NOT NULL DEFAULT 0, + manufacturer_name TEXT, + manufacturer_number TEXT, + category TEXT, + units TEXT, + covetrus_sku TEXT, + mwi_sku TEXT, + patterson_sku TEXT, + midwest_sku TEXT, + first_vet_sku TEXT, + penn_vet_sku TEXT, + amatheon_sku TEXT, + victor_sku TEXT, + vetcove_sku TEXT, + miller_vet_sku TEXT, + boehringer_ingelheim_sku TEXT, + zoetis_sku TEXT, + pharmsource_ah_sku TEXT, + ne_animal_health_sku TEXT, + dechra_sku TEXT, + medline_sku TEXT, + elanco_sku TEXT +); + +CREATE TABLE IF NOT EXISTS VetcoveItemToInventoryItemMap +( + vetcove_item_id INTEGER NOT NULL UNIQUE, + inventory_item_id INTEGER NOT NULL, + FOREIGN KEY(vetcove_item_id) REFERENCES VetcoveItem(vetcove_item_id) + FOREIGN KEY(inventory_item_id) REFERENCES InventoryItem(id) ); -CREATE TABLE IF NOT EXISTS SkuElanco -( - sku TEXT NOT NULL UNIQUE, - item INTEGER, - FOREIGN KEY(item) REFERENCES InventoryItem(id) -); CREATE TABLE IF NOT EXISTS VetcoveItemHistory ( - id INTEGER PRIMARY KEY NOT NULL, - vetcove_entry_id INTEGER NOT NULL UNIQUE, + vetcove_history_entry_id INTEGER PRIMARY KEY NOT NULL, vetcove_item_id INTEGER NOT NULL, order_date TEXT NOT NULL, item_name TEXT NOT NULL, @@ -223,10 +130,10 @@ CREATE TABLE IF NOT EXISTS VetcoveItemHistory supplier_sku TEXT ); -CREATE TABLE IF NOT EXISTS VetcoveItemHistoryToAdjustmentMapping +CREATE TABLE IF NOT EXISTS VetcoveItemHistoryToAdjustmentMap ( + vetcove_history_entry_id INTEGER NOT NULL UNIQUE, adjustment_id INTEGER NOT NULL UNIQUE, - vetcove_item_history_id INTEGER NOT NULL UNIQUE, + FOREIGN KEY(vetcove_history_entry_id) REFERENCES VetcoveItemHistory(vetcove_history_entry_id) FOREIGN KEY(adjustment_id) REFERENCES Adjustment(id) - FOREIGN KEY(vetcove_item_history_id) REFERENCES VetcoveItemHistory(id) ); diff --git a/src/app/item/create.rs b/src/app/item/create.rs index 5222680..a2bc2c8 100644 --- a/src/app/item/create.rs +++ b/src/app/item/create.rs @@ -24,7 +24,6 @@ pub struct CreateItemFormTemplate { pub display_unit_value: StringField, pub reorder_point: StringField, pub pims_id: StringField, - pub vetcove_id: StringField, pub allow_fractional_units: BoolField, } @@ -50,7 +49,6 @@ pub struct CreateItemFormData { display_unit: String, reorder_point: f64, pims_id: Option, - vetcove_id: Option, #[serde(default, deserialize_with = "deserialize_form_checkbox")] allow_fractional_units: bool, } @@ -80,16 +78,12 @@ impl ValidateForm for FormBase { let pims_id = StringField::new(self.form_data.pims_id.clone().unwrap_or_default()) .invalid_if(|v| v.chars().any(char::is_whitespace), FieldError::ValidIdentifier); - let vetcove_id = StringField::new(self.form_data.vetcove_id.clone().unwrap_or_default()) - .invalid_if(|v| !v.chars().all(|c| c.is_ascii_digit()), FieldError::ValidIdentifier); - let allow_fractional_units = BoolField::new(self.form_data.allow_fractional_units); if name.is_error() || display_unit_value.is_error() || reorder_point.is_error() || - pims_id.is_error() || - vetcove_id.is_error() { + pims_id.is_error() { return Err(ValidateFormError::ValidationError( Self::ValidationErrorResponse { @@ -98,7 +92,6 @@ impl ValidateForm for FormBase { display_unit_value, reorder_point, pims_id, - vetcove_id, allow_fractional_units, })); } @@ -118,7 +111,7 @@ pub async fn create_item_form_post( let _new_id = db::inventory_item::add_inventory_item(&state.db, &form_data.name, form_data.reorder_point, form_data.allow_fractional_units, &form_data.display_unit, - &form_data.pims_id, &form_data.vetcove_id, + &form_data.pims_id, ).await?; let fresh_form = CreateItemFormTemplate::base_response(&state).await?; diff --git a/src/app/item/edit.rs b/src/app/item/edit.rs index 8afae5a..f23b3a5 100644 --- a/src/app/item/edit.rs +++ b/src/app/item/edit.rs @@ -26,7 +26,6 @@ pub struct EditItemFormTemplate { pub display_unit_value: EditStringField, pub reorder_point: EditStringField, pub pims_id: EditStringField, - pub vetcove_id: EditStringField, pub allow_fractional_units: EditBoolField, } @@ -34,7 +33,6 @@ impl EditItemFormTemplate { pub fn base(item: DbInventoryItemEditableFields, display_units: Vec) -> Self { let name = EditStringField::new(item.name); let pims_id = EditStringField::from(item.pims_id); - let vetcove_id = EditStringField::from(item.vetcove_id); let allow_fractional_units = EditBoolField::new(item.allow_fractional_units); let display_unit_value = EditStringField::new(item.display_unit_abbreviation); @@ -47,7 +45,6 @@ impl EditItemFormTemplate { name, reorder_point, pims_id, - vetcove_id, allow_fractional_units, display_unit_value, display_units, @@ -107,12 +104,6 @@ impl ValidateForm for FormWithPathVars { ) .invalid_if(|v| v.chars().any(char::is_whitespace), FieldError::ValidIdentifier); - let vetcove_id = EditStringField::new_with_base( - self.form_data.vetcove_id.clone().unwrap_or_default(), - current_values.vetcove_id.unwrap_or_default(), - ) - .invalid_if(|v| !v.chars().all(|c| c.is_ascii_digit()), FieldError::ValidIdentifier); - let allow_fractional_units = EditBoolField::new_with_base( self.form_data.allow_fractional_units, current_values.allow_fractional_units, @@ -121,8 +112,7 @@ impl ValidateForm for FormWithPathVars { if name.is_error() || display_unit_value.is_error() || reorder_point.is_error() || - pims_id.is_error() || - vetcove_id.is_error() { + pims_id.is_error() { return Err(ValidateFormError::ValidationError( Self::ValidationErrorResponse { @@ -132,7 +122,6 @@ impl ValidateForm for FormWithPathVars { display_unit_value, reorder_point, pims_id, - vetcove_id, allow_fractional_units, })); } @@ -153,7 +142,7 @@ pub async fn edit_item_form_post( db::inventory_item::update_inventory_item(&state.db, id, &form_data.name, form_data.reorder_point, form_data.allow_fractional_units, &form_data.display_unit, - &form_data.pims_id, &form_data.vetcove_id, true, + &form_data.pims_id, true, ).await?; let new_base = db::inventory_item::inventory_item_get_by_id_editable_fields(&state.db, id).await?; diff --git a/src/app/routes.rs b/src/app/routes.rs index aa127e0..6711209 100644 --- a/src/app/routes.rs +++ b/src/app/routes.rs @@ -48,6 +48,7 @@ pub fn routes() -> Router { .route("/upload", get(upload::index::upload_index_handler)) .route("/upload/catalog", post(upload::catalog::catalog_import)) .route("/upload/vetcove/history", post(upload::vetcove::item_history_import)) + .route("/upload/vetcove/items", post(upload::vetcove::vetcove_item_import)) .route("/overview", get(overview::overview_handler)) .route("/reports", get(reports::reports_handler)) .route("/history", get(history::history_log_handler)) diff --git a/src/app/upload/index.rs b/src/app/upload/index.rs index 39fa614..afe275c 100644 --- a/src/app/upload/index.rs +++ b/src/app/upload/index.rs @@ -3,7 +3,7 @@ use askama_axum::{IntoResponse, Response}; use crate::error::{AppError}; #[derive(Template)] -#[template(path = "upload.html")] +#[template(path = "upload/upload.html")] struct UploadIndexTemplate; pub async fn upload_index_handler() -> Result { diff --git a/src/app/upload/vetcove.rs b/src/app/upload/vetcove.rs index da7cb3d..2cd82f5 100644 --- a/src/app/upload/vetcove.rs +++ b/src/app/upload/vetcove.rs @@ -2,19 +2,55 @@ use axum::extract::{Multipart, State}; use sqlx::SqlitePool; use askama_axum::{IntoResponse, Response}; use anyhow::anyhow; +use askama::Template; +use axum::Json; use tracing::info; use crate::error::AppError; use crate::session::SessionUser; -use crate::ingest::vetcove::ingest_item_history_bytes; +use crate::ingest::vetcove::{ingest_item_history_bytes, ItemHistoryImportRowResult, ItemHistoryImportRowErrorKind, VetcoveItemImportRowResult, ingest_vetcove_items_bytes}; + +#[derive(Template)] +#[template(path = "upload/vetcove-item-import-results.html")] +struct VetcoveItemImportResultsTemplate { + pub results: Vec, +} + +pub async fn vetcove_item_import( + State(db): State, + user: SessionUser, + mut multipart: Multipart, +) -> Result { + let mut results = vec![]; + while let Some(field) = multipart.next_field().await? { + let filename = field.file_name().ok_or(anyhow!("field missing filename"))?.to_string(); + + let name = field.name().ok_or(anyhow!("field missing name"))?.to_string(); + let content_type = field.content_type().ok_or(anyhow!("field missing content type"))?.to_string(); + let data = field.bytes().await?; + + info!("Name: {}, file: {}, content: {}, data: {} bytes", name, filename, content_type, data.len()); + + results = ingest_vetcove_items_bytes(data, db.clone(), user.id).await?; + } + + Ok(VetcoveItemImportResultsTemplate { results }.into_response()) +} + + +#[derive(Template)] +#[template(path = "upload/vetcove-history-import-results.html")] +struct ItemHistoryImportResultsTemplate { + pub results: Vec, +} pub async fn item_history_import( State(db): State, user: SessionUser, mut multipart: Multipart, ) -> Result { - let mut filename = "".to_owned(); + let mut results = vec![]; while let Some(field) = multipart.next_field().await? { - filename = field.file_name().ok_or(anyhow!("field missing filename"))?.to_string(); + let filename = field.file_name().ok_or(anyhow!("field missing filename"))?.to_string(); let name = field.name().ok_or(anyhow!("field missing name"))?.to_string(); let content_type = field.content_type().ok_or(anyhow!("field missing content type"))?.to_string(); @@ -22,7 +58,10 @@ pub async fn item_history_import( info!("Name: {}, file: {}, content: {}, data: {} bytes", name, filename, content_type, data.len()); - ingest_item_history_bytes(data, db.clone(), user.id).await?; + results = ingest_item_history_bytes(data, db.clone(), user.id).await?; } - Ok(format!("File {} uploaded successfully", filename).into_response()) + + Ok(ItemHistoryImportResultsTemplate { results }.into_response()) } + + diff --git a/src/db/inventory_item.rs b/src/db/inventory_item.rs index a88078d..11871ed 100644 --- a/src/db/inventory_item.rs +++ b/src/db/inventory_item.rs @@ -13,9 +13,6 @@ pub struct DbInventoryItem { pub display_unit: i64, pub active: bool, pub pims_id: Option, - pub vetcove_id: Option, - pub manufacturer_name: Option, - pub manufacturer_id: Option, } @@ -31,10 +28,7 @@ pub async fn inventory_item_get_all(db: &SqlitePool, page_size: i64, page_num: i allow_fractional_units, display_unit, active, - pims_id, - vetcove_id, - manufacturer_name, - manufacturer_id + pims_id FROM InventoryItem LIMIT ? OFFSET ? @@ -62,10 +56,7 @@ pub async fn inventory_item_get_search(db: &SqlitePool, allow_fractional_units, display_unit, active, - pims_id, - vetcove_id, - manufacturer_name, - manufacturer_id + pims_id FROM InventoryItem WHERE InventoryItem.name LIKE ? @@ -89,10 +80,7 @@ pub async fn inventory_item_get_by_id(db: &SqlitePool, id: i64) -> Result Result, vetcove_id: &Option, + pims_id: &Option, ) -> Result { let res = sqlx::query!( r#" - INSERT INTO InventoryItem (name, reorder_point, allow_fractional_units, display_unit, active, pims_id, vetcove_id) - VALUES (?, ?, ?, (SELECT id from DisplayUnit WHERE abbreviation = ? ), ?, ?, ?) + INSERT INTO InventoryItem (name, reorder_point, allow_fractional_units, display_unit, active, pims_id) + VALUES (?, ?, ?, (SELECT id from DisplayUnit WHERE abbreviation = ? ), ?, ?) "#, - name, reorder_point, allow_fractional_units, display_unit_abbreviation, true, pims_id, vetcove_id + name, reorder_point, allow_fractional_units, display_unit_abbreviation, true, pims_id ).execute(db).await?; let new_id = res.last_insert_rowid(); @@ -221,7 +209,6 @@ pub struct DbInventoryItemEditableFields { pub display_unit_abbreviation: String, pub active: bool, pub pims_id: Option, - pub vetcove_id: Option, } pub async fn inventory_item_get_by_id_editable_fields(db: &SqlitePool, id: i64) -> Result { @@ -237,8 +224,7 @@ pub async fn inventory_item_get_by_id_editable_fields(db: &SqlitePool, id: i64) display_unit.unit as display_unit_str, display_unit.abbreviation as display_unit_abbreviation, item.active as active, - item.pims_id as pims_id, - item.vetcove_id as vetcove_id + item.pims_id as pims_id FROM InventoryItem as item JOIN DisplayUnit as display_unit ON item.display_unit = display_unit.id @@ -252,7 +238,7 @@ pub async fn inventory_item_get_by_id_editable_fields(db: &SqlitePool, id: i64) pub async fn update_inventory_item(db: &SqlitePool, id: i64, name: &str, reorder_point: f64, allow_fractional_units: bool, display_unit_abbreviation: &str, - pims_id: &Option, vetcove_id: &Option, active: bool + pims_id: &Option, active: bool ) -> Result<()> { let affected = sqlx::query!( r#" @@ -262,8 +248,7 @@ pub async fn update_inventory_item(db: &SqlitePool, id: i64, name: &str, reorder allow_fractional_units = ?, display_unit = (SELECT id from DisplayUnit WHERE abbreviation = ? ), active = ?, - pims_id = ?, - vetcove_id = ? + pims_id = ? WHERE id = ? "#, name, @@ -272,7 +257,6 @@ pub async fn update_inventory_item(db: &SqlitePool, id: i64, name: &str, reorder display_unit_abbreviation, active, pims_id, - vetcove_id, id ).execute(db).await?.rows_affected(); diff --git a/src/db/vetcove/item.rs b/src/db/vetcove/item.rs new file mode 100644 index 0000000..c72cfed --- /dev/null +++ b/src/db/vetcove/item.rs @@ -0,0 +1,274 @@ +use serde::Serialize; +use anyhow::Result; +use chrono::{DateTime, NaiveDate, Utc}; +use sqlx::SqlitePool; + +#[derive(Clone, Debug, Serialize)] +#[derive(sqlx::FromRow)] +pub struct DbVetcoveItem { + pub vetcove_item_id: i64, + pub item_name: String, + pub ignored: i64, + pub manufacturer_name: Option, + pub manufacturer_number: Option, + pub category: Option, + pub units: Option, + pub covetrus_sku: Option, + pub mwi_sku: Option, + pub patterson_sku: Option, + pub midwest_sku: Option, + pub first_vet_sku: Option, + pub penn_vet_sku: Option, + pub amatheon_sku: Option, + pub victor_sku: Option, + pub vetcove_sku: Option, + pub miller_vet_sku: Option, + pub boehringer_ingelheim_sku: Option, + pub zoetis_sku: Option, + pub pharmsource_ah_sku: Option, + pub ne_animal_health_sku: Option, + pub dechra_sku: Option, + pub medline_sku: Option, + pub elanco_sku: Option, +} + + +pub async fn add_vetcove_item(db: &SqlitePool, + vetcove_item_id: i64, + item_name: String, + ignored: bool, + manufacturer_name: Option, + manufacturer_number: Option, + category: Option, + units: Option, + covetrus_sku: Option, + mwi_sku: Option, + patterson_sku: Option, + midwest_sku: Option, + first_vet_sku: Option, + penn_vet_sku: Option, + amatheon_sku: Option, + victor_sku: Option, + vetcove_sku: Option, + miller_vet_sku: Option, + boehringer_ingelheim_sku: Option, + zoetis_sku: Option, + pharmsource_ah_sku: Option, + ne_animal_health_sku: Option, + dechra_sku: Option, + medline_sku: Option, + elanco_sku: Option) + -> Result> { + + let existing: i64 = sqlx::query_scalar(r#" + SELECT COUNT(1) FROM VetcoveItem WHERE vetcove_item_id = ? + "#).bind(vetcove_item_id).fetch_one(db).await?; + + if existing > 0 { + return Ok(None) + } + + let res = sqlx::query( + r#" + INSERT INTO VetcoveItem ( + vetcove_item_id, + item_name, + ignored, + manufacturer_name, + manufacturer_number, + category, + units, + covetrus_sku, + mwi_sku, + patterson_sku, + midwest_sku, + first_vet_sku, + penn_vet_sku, + amatheon_sku, + victor_sku, + vetcove_sku, + miller_vet_sku, + boehringer_ingelheim_sku, + zoetis_sku, + pharmsource_ah_sku, + ne_animal_health_sku, + dechra_sku, + medline_sku, + elanco_sku) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "#) + .bind(vetcove_item_id) + .bind(item_name) + .bind(ignored) + .bind(manufacturer_name) + .bind(manufacturer_number) + .bind(category) + .bind(units) + .bind(covetrus_sku) + .bind(mwi_sku) + .bind(patterson_sku) + .bind(midwest_sku) + .bind(first_vet_sku) + .bind(penn_vet_sku) + .bind(amatheon_sku) + .bind(victor_sku) + .bind(vetcove_sku) + .bind(miller_vet_sku) + .bind(boehringer_ingelheim_sku) + .bind(zoetis_sku) + .bind(pharmsource_ah_sku) + .bind(ne_animal_health_sku) + .bind(dechra_sku) + .bind(medline_sku) + .bind(elanco_sku) + .execute(db).await?; + + let _new_id = res.last_insert_rowid(); + + Ok(Some(vetcove_item_id)) +} + +pub async fn get_vetcove_item_by_id(db: &SqlitePool, vetcove_item_id: i64) -> Result> { + sqlx::query_as!( + DbVetcoveItem, + r#" + SELECT + vetcove_item_id, + item_name, + ignored, + manufacturer_name, + manufacturer_number, + category, + units, + covetrus_sku, + mwi_sku, + patterson_sku, + midwest_sku, + first_vet_sku, + penn_vet_sku, + amatheon_sku, + victor_sku, + vetcove_sku, + miller_vet_sku, + boehringer_ingelheim_sku, + zoetis_sku, + pharmsource_ah_sku, + ne_animal_health_sku, + dechra_sku, + medline_sku, + elanco_sku + FROM + VetcoveItem + WHERE VetcoveItem.vetcove_item_id = ? + "#, + vetcove_item_id + ) + .fetch_optional(db).await + .map_err(From::from) +} + +pub async fn get_vetcove_item_ignored(db: &SqlitePool, vetcove_item_id: i64) + -> Result { + + let query_res: i64 = sqlx::query_scalar(r#" + SELECT ignored from VetcoveItem WHERE vetcove_item_id = ? + "#) + .bind(vetcove_item_id) + .fetch_one(db).await?; + + let ignored = query_res != 0; + + Ok(ignored) +} + +pub async fn set_vetcove_item_ignored(db: &SqlitePool, vetcove_item_id: i64, ignored: bool) + -> Result<()> { + + let affected = sqlx::query(r#" + UPDATE VetcoveItem SET ignored = ? WHERE vetcove_item_id = ? + "#) + .bind(ignored) + .bind(vetcove_item_id) + .execute(db).await?.rows_affected(); + + assert_eq!(affected, 1); + + Ok(()) +} + +pub async fn add_vetcove_item_to_inventory_item_mapping(db: &SqlitePool, + vetcove_item_id: i64, + inventory_item_id: i64) + -> Result { + let res = sqlx::query( + r#" + INSERT INTO VetcoveItemToInventoryItemMap ( + vetcove_item_id, + inventory_item_id + ) + VALUES (?, ?) + "#) + .bind(vetcove_item_id) + .bind(inventory_item_id) + .execute(db).await?; + + let new_id = res.last_insert_rowid(); + + Ok(new_id) +} + +pub async fn map_vetcove_item_to_inventory_item(db: &SqlitePool, + vetcove_item_id: i64, + inventory_item_id: i64) + -> Result<()> { + + let existing: i64 = sqlx::query_scalar(r#" + SELECT COUNT(1) FROM VetcoveItemToInventoryItemMap WHERE vetcove_item_id = ? + "#).bind(vetcove_item_id).fetch_one(db).await?; + + if existing > 0 { + let res = sqlx::query(r#" + UPDATE VetcoveItemToInventoryItemMap + SET inventory_item_id = ? + WHERE vetcove_item_id = ? + "#) + .bind(inventory_item_id) + .bind(vetcove_item_id) + .execute(db).await?.rows_affected(); + + assert_eq!(res, 1); + } + else { + let res = sqlx::query(r#" + INSERT INTO VetcoveItemToInventoryItemMap (vetcove_item_id, inventory_item_id) + VALUES (?, ?) + "#) + .bind(vetcove_item_id) + .bind(inventory_item_id) + .execute(db).await?.rows_affected(); + + assert_eq!(res, 1); + } + + Ok(()) +} + +pub async fn get_inventory_item_id_by_vetcove_item_id(db: &SqlitePool, vetcove_item_id: i64) + -> Result> { + + let value: Option = sqlx::query_scalar(r#" + SELECT inventory_item_id FROM VetcoveItemToInventoryItemMap WHERE vetcove_item_id = ? + "#).bind(vetcove_item_id).fetch_optional(db).await?; + + Ok(value) +} + +pub async fn get_vetcove_item_ids_by_inventory_item_id(db: &SqlitePool, inventory_item_id: i64) + -> Result> { + + let value: Vec = sqlx::query_scalar(r#" + SELECT inventory_item_id FROM VetcoveItemToInventoryItemMap WHERE vetcove_item_id = ? + "#).bind(inventory_item_id).fetch_all(db).await?; + + Ok(value) +} diff --git a/src/db/vetcove/item_history.rs b/src/db/vetcove/item_history.rs index 05eb9e6..0fe5dc6 100644 --- a/src/db/vetcove/item_history.rs +++ b/src/db/vetcove/item_history.rs @@ -34,7 +34,7 @@ pub struct DbVetcoveItemHistoryEntry { pub async fn add_vetcove_item_history_entry(db: &SqlitePool, - vetcove_entry_id: i64, + vetcove_history_entry_id: i64, vetcove_item_id: i64, order_date: NaiveDate, item_name: String, @@ -58,8 +58,8 @@ pub async fn add_vetcove_item_history_entry(db: &SqlitePool, -> Result> { let existing: i64 = sqlx::query_scalar(r#" - SELECT COUNT(1) FROM VetcoveItemHistory WHERE vetcove_entry_id = ? - "#).bind(vetcove_entry_id).fetch_one(db).await?; + SELECT COUNT(1) FROM VetcoveItemHistory WHERE vetcove_history_entry_id = ? + "#).bind(vetcove_history_entry_id).fetch_one(db).await?; if existing > 0 { return Ok(None) @@ -68,7 +68,7 @@ pub async fn add_vetcove_item_history_entry(db: &SqlitePool, let res = sqlx::query( r#" INSERT INTO VetcoveItemHistory ( - vetcove_entry_id, + vetcove_history_entry_id, vetcove_item_id, order_date, item_name, @@ -91,7 +91,7 @@ pub async fn add_vetcove_item_history_entry(db: &SqlitePool, supplier_sku ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "#) - .bind(vetcove_entry_id) + .bind(vetcove_history_entry_id) .bind(vetcove_item_id) .bind(order_date) .bind(item_name) @@ -114,7 +114,28 @@ pub async fn add_vetcove_item_history_entry(db: &SqlitePool, .bind(supplier_sku) .execute(db).await?; + let _new_id = res.last_insert_rowid(); + + Ok(Some(vetcove_history_entry_id)) +} + +pub async fn add_item_history_to_adjustment_mapping(db: &SqlitePool, + vetcove_history_entry_id: i64, + adjustment_id: i64) + -> Result { + let res = sqlx::query( + r#" + INSERT INTO VetcoveItemHistoryToAdjustmentMap ( + vetcove_history_entry_id, + adjustment_id + ) + VALUES (?, ?) + "#) + .bind(vetcove_history_entry_id) + .bind(adjustment_id) + .execute(db).await?; + let new_id = res.last_insert_rowid(); - Ok(Some(new_id)) + Ok(new_id) } diff --git a/src/db/vetcove/mod.rs b/src/db/vetcove/mod.rs index b46226f..de3b37a 100644 --- a/src/db/vetcove/mod.rs +++ b/src/db/vetcove/mod.rs @@ -1 +1,2 @@ -pub mod item_history; \ No newline at end of file +pub mod item_history; +pub mod item; \ No newline at end of file diff --git a/src/ingest/catalog.rs b/src/ingest/catalog.rs index 0d49004..392701d 100644 --- a/src/ingest/catalog.rs +++ b/src/ingest/catalog.rs @@ -15,6 +15,8 @@ struct CatalogRecord { reorder_point: f64, #[serde(alias = "price")] unit_price: i64, + #[serde(alias = "vetcove id")] + vetcove_id: String, } pub async fn ingest_catalog_bytes(bytes: Bytes, db: SqlitePool, user_id: i64) -> anyhow::Result<()> { @@ -36,7 +38,6 @@ pub async fn ingest_catalog(mut reader: csv::Reader, db: Sq record.fractional, &record.unit, &None, - &None, ).await?; let new_positive_adjustment = add_adjustment_new_stock(&db, new_entry_id, diff --git a/src/ingest/vetcove.rs b/src/ingest/vetcove.rs index a49ade1..d669c6a 100644 --- a/src/ingest/vetcove.rs +++ b/src/ingest/vetcove.rs @@ -1,69 +1,220 @@ - +use anyhow::anyhow; use sqlx::SqlitePool; -use tracing::info; +use tracing::{error, info}; use axum::body::Bytes; -use crate::db::adjustment::add_adjustment_new_stock; -use crate::db::inventory_item::add_inventory_item; -use crate::db::vetcove::item_history::add_vetcove_item_history_entry; +use crate::db::adjustment::{add_adjustment, add_adjustment_new_stock}; +use crate::db::adjustment::adjustment_reason::DbAdjustmentReason; +use crate::db::vetcove::item_history::{add_item_history_to_adjustment_mapping, add_vetcove_item_history_entry}; +use crate::db::vetcove::item::{add_vetcove_item, get_inventory_item_id_by_vetcove_item_id, get_vetcove_item_by_id, get_vetcove_item_ignored}; +use crate::util::currency::dollars_string_to_int_cents; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct VetcoveItemRecord { + #[serde(alias = "Vetcove ID")] + pub vetcove_item_id: i64, + #[serde(alias = "Item Name")] + pub item_name: String, + #[serde(alias = "Manufacturer Name")] + pub manufacturer_name: String, + #[serde(alias = "Manufacturer No.")] + pub manufacturer_number: String, + #[serde(alias = "Category")] + pub category: String, + #[serde(alias = "Units")] + pub units: String, + #[serde(alias = "Last Purchase")] + pub last_purchase: String, + #[serde(alias = "Last Purchase List Price")] + pub last_purchase_list_price: String, + #[serde(alias = "Number of Orders")] + pub number_of_orders: String, + #[serde(alias = "Percentage of Total Volume")] + pub percentage_of_total_volume: String, + #[serde(alias = "Quantity")] + pub quantity: String, + #[serde(alias = "Total")] + pub total: String, + #[serde(alias = "Covetrus SKU")] + pub covetrus_sku: String, + #[serde(alias = "MWI SKU")] + pub mwi_sku: String, + #[serde(alias = "Patterson SKU")] + pub patterson_sku: String, + #[serde(alias = "Midwest SKU")] + pub midwest_sku: String, + #[serde(alias = "First Vet SKU")] + pub first_vet_sku: String, + #[serde(alias = "PennVet SKU")] + pub pennvet_sku: String, + #[serde(alias = "Amatheon SKU")] + pub amatheon_sku: String, + #[serde(alias = "Victor SKU")] + pub victor_sku: String, + #[serde(alias = "Vetcove SKU")] + pub vetcove_sku: String, + #[serde(alias = "Miller Vet SKU")] + pub miller_vet_sku: String, + #[serde(alias = "Boehringer Ingelheim SKU")] + pub boehringer_ingelheim_sku: String, + #[serde(alias = "Zoetis SKU")] + pub zoetis_sku: String, + #[serde(alias = "Pharmsource AH SKU")] + pub pharmsource_ah_sku: String, + #[serde(alias = "NE Animal Health SKU")] + pub ne_animal_health_sku: String, + #[serde(alias = "Dechra SKU")] + pub dechra_sku: String, + #[serde(alias = "Medline SKU")] + pub medline_sku: String, + #[serde(alias = "Elanco SKU")] + pub elanco_sku: String, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct VetcoveItemImportRowResult { + pub already_imported: bool, + pub item: VetcoveItemRecord, + pub item_is_ignored: bool, +} + + +pub async fn ingest_vetcove_items_bytes(bytes: Bytes, db: SqlitePool, user_id: i64) -> anyhow::Result> { + let reader = csv::Reader::from_reader(bytes.as_ref()); + + ingest_vetcove_items(reader, db, user_id).await +} + +pub async fn ingest_vetcove_items(mut reader: csv::Reader, db: SqlitePool, user_id: i64) -> anyhow::Result> +{ + let timestamp = chrono::Utc::now(); + let mut import_results = vec![]; + + for result in reader.deserialize() { + let record: VetcoveItemRecord = result?; + let vetcove_item_id = record.vetcove_item_id; + let mut row_result = VetcoveItemImportRowResult { + already_imported: false, item: record.clone(), item_is_ignored: false + }; + + let query_res = add_vetcove_item( + &db, + record.vetcove_item_id, + record.item_name, + false, + Some(record.manufacturer_name), + Some(record.manufacturer_number), + Some(record.category), + Some(record.units), + Some(record.covetrus_sku), + Some(record.mwi_sku), + Some(record.patterson_sku), + Some(record.midwest_sku), Some(record.first_vet_sku), Some(record.pennvet_sku), + Some(record.amatheon_sku), Some(record.victor_sku), Some(record.vetcove_sku), + Some(record.miller_vet_sku), Some(record.boehringer_ingelheim_sku), + Some(record.zoetis_sku), Some(record.pharmsource_ah_sku), + Some(record.ne_animal_health_sku), Some(record.dechra_sku), Some(record.medline_sku), + Some(record.elanco_sku), + ).await?; + + if query_res.is_none() { + info!("Vetcove item already imported, skipping {}", vetcove_item_id); + row_result.already_imported = true; + } + + //NOTE: Might want to grab more info from the db at some point, but for now anything other + // than 'ignored' is duplicate info + let ignored = get_vetcove_item_ignored(&db, vetcove_item_id).await?; + row_result.item_is_ignored = ignored; + + import_results.push(row_result); + } + + Ok(import_results) +} + -#[derive(Debug, serde::Deserialize)] -struct ItemHistoryRecord { +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct VetcoveItemHistoryRecord { #[serde(alias = "Vetcove ID")] - vetcove_entry_id: i64, + pub vetcove_entry_id: i64, #[serde(alias = "Date")] - order_date: chrono::NaiveDate, + pub order_date: chrono::NaiveDate, #[serde(alias = "Hospital")] - hospital: String, + pub hospital: String, #[serde(alias = "Name")] - item_name: String, + pub item_name: String, #[serde(alias = "Order #")] - order_id: String, + pub order_id: String, #[serde(alias = "PO Number")] - po_number: String, + pub po_number: String, #[serde(alias = "Supplier")] - supplier: String, + pub supplier: String, #[serde(alias = "Manufacturer")] - manufacturer: String, + pub manufacturer: String, #[serde(alias = "Manufacturer Number")] - manufacturer_number: String, + pub manufacturer_number: String, #[serde(alias = "Primary Category")] - primary_category: String, + pub primary_category: String, #[serde(alias = "Secondary Category")] - secondary_category: String, + pub secondary_category: String, #[serde(alias = "Vetcove Item ID")] - vetcove_item_id: i64, + pub vetcove_item_id: i64, #[serde(alias = "Cost per Dose")] - cost_per_dose: String, + pub cost_per_dose: String, #[serde(alias = "Units")] - units: f64, + pub units: f64, #[serde(alias = "Unit Price")] - unit_price: String, + pub unit_price: String, #[serde(alias = "Unit Measurement")] - unit_measurement: String, + pub unit_measurement: String, #[serde(alias = "List Price")] - list_price: String, + pub list_price: String, #[serde(alias = "Quantity")] - quantity: f64, + pub quantity: f64, #[serde(alias = "Total Price")] - total_price: String, + pub total_price: String, #[serde(alias = "Item Status")] - item_status: String, + pub item_status: String, #[serde(alias = "Supplier's SKU")] - supplier_sku: String, + pub supplier_sku: String, } -pub async fn ingest_item_history_bytes(bytes: Bytes, db: SqlitePool, user_id: i64) -> anyhow::Result<()> { +#[derive(Debug, Clone, Copy, serde::Serialize)] +pub enum ItemHistoryImportRowErrorKind { + AlreadyImported, + VetcoveIdNotFound, +} + + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ItemHistoryImportRowResult { + pub error: Option, + pub item: VetcoveItemHistoryRecord, +} + +pub async fn ingest_item_history_bytes(bytes: Bytes, db: SqlitePool, user_id: i64) -> anyhow::Result> { let reader = csv::Reader::from_reader(bytes.as_ref()); ingest_item_history(reader, db, user_id).await } -pub async fn ingest_item_history(mut reader: csv::Reader, db: SqlitePool, user_id: i64) -> anyhow::Result<()> +pub async fn ingest_item_history(mut reader: csv::Reader, db: SqlitePool, user_id: i64) -> anyhow::Result> { let timestamp = chrono::Utc::now(); + let mut import_results = vec![]; for result in reader.deserialize() { - let record: ItemHistoryRecord = result?; + let record: VetcoveItemHistoryRecord = result?; + let mut row_result = ItemHistoryImportRowResult { error: None, item: record.clone() }; + + let vetcove_item = get_vetcove_item_by_id(&db, record.vetcove_item_id).await?; + + if vetcove_item.is_none() { + info!("Unable to add entry, no matching vetcove ID {}", record.vetcove_entry_id); + row_result.error = Some(ItemHistoryImportRowErrorKind::VetcoveIdNotFound); + import_results.push(row_result); + continue; + } let query_res = add_vetcove_item_history_entry( &db, @@ -90,10 +241,13 @@ pub async fn ingest_item_history(mut reader: csv::Reader, d Some(record.supplier_sku), ).await?; - match query_res { - Some(new_db_id) => info!("Added new history item: {} => {}", record.vetcove_entry_id, new_db_id), - None => info!("History item exists, skipping {}", record.vetcove_entry_id), + if query_res.is_none() { + info!("History item exists, skipping {}", record.vetcove_entry_id); + row_result.error = Some(ItemHistoryImportRowErrorKind::AlreadyImported); } + + import_results.push(row_result); } - Ok(()) + + Ok(import_results) } diff --git a/templates/item/item-create-form.html b/templates/item/item-create-form.html index 3a39261..4344f6e 100644 --- a/templates/item/item-create-form.html +++ b/templates/item/item-create-form.html @@ -108,26 +108,6 @@ {% endif -%} -
- - - {% if vetcove_id.is_error() -%} - - {{ vetcove_id.error_string() }} - - {% endif -%} -
- -{% endblock %} diff --git a/templates/upload/upload.html b/templates/upload/upload.html new file mode 100644 index 0000000..957b989 --- /dev/null +++ b/templates/upload/upload.html @@ -0,0 +1,91 @@ +{% extends "main.html" %} {% block title %} Upload {% endblock %} {% block +content %} + +
+
+
+

Catalog Import

+ + + +
+
+ +
+

Vetcove Item History

+ + + + +
+ +
+

Vetcove Items

+ + + + +
+ +
+
+ +{% endblock %} diff --git a/templates/upload/vetcove-history-import-results.html b/templates/upload/vetcove-history-import-results.html new file mode 100644 index 0000000..8c41ae2 --- /dev/null +++ b/templates/upload/vetcove-history-import-results.html @@ -0,0 +1,39 @@ + + + + + + + + + + {% for res in results %} + {% match res.error %} + {% when Some(ItemHistoryImportRowErrorKind::AlreadyImported) %} + + + + + + {% when Some(ItemHistoryImportRowErrorKind::VetcoveIdNotFound) %} + + + + + + {% when None %} + + + + + + {% else %} + + + + + + {% endmatch %} + {% endfor %} + +
StatusVetcove IDName
Skip {{ res.item.vetcove_item_id }}{{ res.item.item_name }}
Id Not Found {{ res.item.vetcove_item_id }}{{ res.item.item_name }}
Success {{ res.item.vetcove_item_id }}{{ res.item.item_name }}
Unknown Error {{ res.item.vetcove_item_id }}{{ res.item.item_name }}
diff --git a/templates/upload/vetcove-item-import-results.html b/templates/upload/vetcove-item-import-results.html new file mode 100644 index 0000000..fb3680f --- /dev/null +++ b/templates/upload/vetcove-item-import-results.html @@ -0,0 +1,26 @@ + + + + + + + + + + {% for res in results %} + {% if res.already_imported %} + + + + + + {% else %} + + + + + + {% endif %} + {% endfor %} + +
StatusVetcove IDName
Skip {{ res.item.vetcove_item_id }}{{ res.item.item_name }}
Imported {{ res.item.vetcove_item_id }}{{ res.item.item_name }}