1use super::client::{PlmClient, PlmError};
18use crate::store::DocumentIdentity;
19use serde::Deserialize;
20use serde_json::{json, Value};
21
22#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
24#[serde(default)]
25pub struct ServerBom {
26 pub comparison_lines: Vec<ServerLine>,
27 pub part_id: String,
28 pub number: String,
29 pub name: String,
30 pub revision_id: String,
31 pub revision_label: String,
32 pub state: String,
33 pub flat: bool,
34 pub levels: usize,
35 pub lines: Vec<ServerLine>,
36 pub totals: Vec<CurrencyTotal>,
37 pub unpriced: usize,
38 pub warnings: Vec<String>,
39}
40
41#[derive(Clone, Debug, Default, PartialEq, Deserialize, serde::Serialize)]
43#[serde(default)]
44pub struct ServerLine {
45 pub part_type: String,
46 pub part_values: std::collections::BTreeMap<String,Value>,
47 pub owner_part: String,
48 pub owner_revision: String,
49 pub occurrence_ids: Vec<String>,
50 pub occurrence_attributes: std::collections::BTreeMap<String,Value>,
51 pub notes: String,
52 pub level: usize,
53 pub position: String,
54 pub find_number: String,
55 pub reference: String,
56 pub part_id: String,
57 pub number: String,
58 pub name: String,
59 pub revision_id: String,
60 pub revision_label: String,
61 pub state: String,
62 pub floating: bool,
63 pub category_path: String,
64 pub attributes: Vec<String>,
65 pub quantity: f64,
66 pub unit: String,
67 pub total: f64,
68 pub assembly: bool,
69 pub mpn: String,
70 pub manufacturer: String,
71 pub supplier: String,
72 pub spn: String,
73 pub currency: String,
74 pub unit_price: Option<f64>,
75 pub extended: Option<f64>,
76 pub costed: bool,
78 pub warnings: Vec<String>,
79 pub flags: Vec<String>,
81}
82
83#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
84#[serde(default)]
85pub struct CurrencyTotal {
86 pub currency: String,
87 pub total: f64,
88 pub lines: usize,
89}
90
91#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
93#[serde(default)]
94pub struct WhereUsed {
95 pub part_id: String,
96 pub number: String,
97 pub revision_label: String,
98 pub levels: usize,
99 pub lines: Vec<WhereUsedLine>,
100}
101
102#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
103#[serde(default)]
104pub struct WhereUsedLine {
105 pub level: usize,
106 pub part_id: String,
107 pub number: String,
108 pub name: String,
109 pub revision_id: String,
110 pub revision_label: String,
111 pub state: String,
112 pub current: bool,
114 pub uses_revision: String,
115 pub floating: bool,
116 pub quantity: f64,
117 pub unit: String,
118 pub find_number: String,
119 pub top: bool,
120}
121
122pub async fn fetch_bom(client: &PlmClient, part: &str, revision: &str, flat: bool) -> Result<ServerBom, PlmError> {
124 let path = format!("{}/bom?levels=0{}", super::identity::revision_path(part, revision), if flat { "&flat=true" } else { "" });
125 json(client.call("GET", &path, None).await?, &path)
126}
127
128pub async fn fetch_occurrence_bom(client:&PlmClient,part:&str,revision:&str,flat:bool)->Result<ServerBom,PlmError>{
129 let path=format!("{}/bom?levels=0&occurrences=true&flat={flat}",super::identity::revision_path(part,revision));
130 json(client.call("GET",&path,None).await?,&path)
131}
132
133pub async fn where_used(client: &PlmClient, part: &str, revision: Option<&str>) -> Result<WhereUsed, PlmError> {
135 let path = match revision {
136 Some(revision) => format!("{}/where-used?levels=0&revision={}", super::identity::part_path(part), super::identity::segment(revision)),
137 None => format!("{}/where-used?levels=0", crate::plm::identity::part_path(&part)),
138 };
139 json(client.call("GET", &path, None).await?, &path)
140}
141
142pub async fn patch_attribute(client: &PlmClient, part: &str, key: &str, value: &Value) -> Result<(), PlmError> {
146 let body = json!({ "attributes": { attribute_key(key): value } });
147 client
148 .call("PATCH", &crate::plm::identity::part_path(&part), Some(body.to_string().into_bytes()))
149 .await
150 .map(|_| ())
151}
152
153pub fn attribute_key(field: &str) -> String {
155 field.trim().to_ascii_lowercase()
156}
157
158fn json<T: for<'de> Deserialize<'de>>(response: super::PlmResponse, what: &str) -> Result<T, PlmError> {
159 serde_json::from_slice(&response.body).map_err(|e| PlmError::Malformed(format!("{what}: {e}")))
160}
161
162pub fn revision_of_document(name: &str) -> Option<(String, String)> {
166 let key = name.trim_start_matches('/');
167 let key = key.strip_prefix("models/").unwrap_or(key);
168 let key = key.strip_suffix(".nbrep").unwrap_or(key);
169 match DocumentIdentity::parse_revision_key(key)? {
170 DocumentIdentity::Revision { part, revision } => Some((part, revision)),
171 DocumentIdentity::Path(_) => None,
172 }
173}
174
175#[derive(Clone, Debug, PartialEq)]
177pub struct DocumentLine {
178 pub level: usize,
179 pub position: String,
181 pub part_id: String,
182 pub revision_id: String,
183 pub quantity: f64,
185 pub total: f64,
187}
188
189pub fn document_bom(document: &Value) -> Result<Vec<DocumentLine>, Vec<String>> {
195 let mut lines = Vec::new();
196 let mut refusals = Vec::new();
197 descend(document, 1, "", 1.0, &mut lines, &mut refusals);
198 if refusals.is_empty() {
199 Ok(lines)
200 } else {
201 Err(refusals)
202 }
203}
204
205fn descend(document: &Value, level: usize, prefix: &str, parent_total: f64, out: &mut Vec<DocumentLine>, refusals: &mut Vec<String>) {
206 let occurrences = super::uses::occurrences(document);
207 let list = super::uses::uses_lines(&occurrences, &super::uses::plm_identity);
208 for refusal in &list.refusals {
209 let at = if prefix.is_empty() { String::new() } else { format!(" (under {prefix})") };
210 refusals.push(format!("{}{at}: {}", refusal.occurrence, refusal.reason));
211 }
212 for (index, placed) in list.lines.iter().enumerate() {
213 let position = if prefix.is_empty() { format!("{}", index + 1) } else { format!("{prefix}.{}", index + 1) };
214 let quantity = f64::from(placed.line.quantity);
215 let total = parent_total * quantity;
216 out.push(DocumentLine {
217 level,
218 position: position.clone(),
219 part_id: placed.line.part.clone(),
220 revision_id: placed.line.revision.clone(),
221 quantity,
222 total,
223 });
224 let entry = placed
227 .occurrences
228 .first()
229 .and_then(|id| occurrences.iter().find(|o| &o.id == id))
230 .and_then(|o| document.get("partsLibrary")?.get(&o.part_name));
231 if let Some(child) = entry.and_then(|entry| entry.get("document")).filter(|d| d.is_object()) {
232 descend(child, level + 1, &position, total, out, refusals);
233 }
234 }
235}
236
237pub fn compare(document: &[DocumentLine], server: &[ServerLine]) -> Vec<String> {
242 let mut differences = Vec::new();
243 let named = |line: &ServerLine| {
244 if line.number.is_empty() {
245 line.part_id.clone()
246 } else {
247 format!("{} rev {}", line.number, line.revision_label)
248 }
249 };
250 for index in 0..document.len().max(server.len()) {
251 match (document.get(index), server.get(index)) {
252 (Some(doc), Some(srv)) => {
253 let mut says = Vec::new();
254 if doc.position != srv.position || doc.level != srv.level {
255 says.push(format!("the document has position {} (level {}), the PLM {} (level {})", doc.position, doc.level, srv.position, srv.level));
256 }
257 if doc.part_id != srv.part_id || doc.revision_id != srv.revision_id {
258 says.push(format!("the document places part {} revision {}", doc.part_id, doc.revision_id));
259 }
260 if (doc.quantity - srv.quantity).abs() > 1e-9 {
261 says.push(format!("quantity {} in the document, {} on the PLM", doc.quantity, srv.quantity));
262 }
263 if (doc.total - srv.total).abs() > 1e-9 {
264 says.push(format!("total {} in the document, {} on the PLM", doc.total, srv.total));
265 }
266 if !says.is_empty() {
267 differences.push(format!("line {} ({}): {}", srv.position, named(srv), says.join("; ")));
268 }
269 }
270 (Some(doc), None) => differences.push(format!(
271 "line {}: the document places part {} revision {}, which the PLM's BOM does not list",
272 doc.position, doc.part_id, doc.revision_id
273 )),
274 (None, Some(srv)) => differences.push(format!("line {} ({}): on the PLM's BOM, not in the document", srv.position, named(srv))),
275 (None, None) => {}
276 }
277 }
278 differences
279}
280
281pub fn show_where_used(ui: &mut eframe::egui::Ui, used: &WhereUsed) {
285 use eframe::egui;
286 if used.lines.is_empty() {
287 ui.label(format!("{} is used by no assembly.", used.number));
288 return;
289 }
290 let current = used.lines.iter().filter(|l| l.current).count();
291 ui.label(format!(
292 "{} is used in {} assembly revision{} ({} current, which a change still reaches):",
293 used.number,
294 used.lines.len(),
295 if used.lines.len() == 1 { "" } else { "s" },
296 current
297 ));
298 egui::Grid::new(("where-used", &used.part_id)).striped(true).show(ui, |ui| {
299 for heading in ["Assembly", "Rev", "State", "Uses", "Qty"] {
300 ui.strong(heading);
301 }
302 ui.end_row();
303 for line in &used.lines {
304 let text = format!("{}{} {}", " ".repeat(line.level.saturating_sub(1)), line.number, line.name);
305 if line.current {
306 ui.label(text);
307 } else {
308 ui.label(egui::RichText::new(text).weak());
309 }
310 ui.label(&line.revision_label);
311 ui.label(&line.state);
312 ui.label(&line.uses_revision);
313 ui.label(format!("{} {}", line.quantity, line.unit));
314 ui.end_row();
315 }
316 });
317}
318