• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inventree / inventree-app / 12327582131

14 Dec 2024 06:22AM UTC coverage: 8.396% (+0.01%) from 8.386%
12327582131

Pull #578

github

web-flow
Merge 0781f4721 into 524c5469f
Pull Request #578: Refactor search widget

6 of 91 new or added lines in 4 files covered. (6.59%)

1 existing line in 1 file now uncovered.

727 of 8659 relevant lines covered (8.4%)

0.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/lib/widget/part/part_detail.dart
1
import "package:flutter/material.dart";
2
import "package:flutter_speed_dial/flutter_speed_dial.dart";
3
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
4

5
import "package:inventree/app_colors.dart";
6
import "package:inventree/barcode/barcode.dart";
7
import "package:inventree/l10.dart";
8
import "package:inventree/helpers.dart";
9

10
import "package:inventree/inventree/bom.dart";
11
import "package:inventree/inventree/part.dart";
12
import "package:inventree/inventree/stock.dart";
13
import "package:inventree/labels.dart";
14
import "package:inventree/preferences.dart";
15

16
import "package:inventree/widget/attachment_widget.dart";
17
import "package:inventree/widget/part/bom_list.dart";
18
import "package:inventree/widget/part/part_list.dart";
19
import "package:inventree/widget/notes_widget.dart";
20
import "package:inventree/widget/part/part_parameter_widget.dart";
21
import "package:inventree/widget/progress.dart";
22
import "package:inventree/widget/part/category_display.dart";
23
import "package:inventree/widget/refreshable_state.dart";
24
import "package:inventree/widget/part/part_image_widget.dart";
25
import "package:inventree/widget/snacks.dart";
26
import "package:inventree/widget/stock/stock_detail.dart";
27
import "package:inventree/widget/stock/stock_list.dart";
28
import "package:inventree/widget/company/supplier_part_list.dart";
29

30

31
/*
32
 * Widget for displaying a detail view of a single Part instance
33
 */
34
class PartDetailWidget extends StatefulWidget {
35

36
  const PartDetailWidget(this.part, {Key? key}) : super(key: key);
×
37

38
  final InvenTreePart part;
39

40
  @override
×
41
  _PartDisplayState createState() => _PartDisplayState(part);
×
42

43
}
44

45

46
class _PartDisplayState extends RefreshableState<PartDetailWidget> {
47

48
  _PartDisplayState(this.part);
×
49

50
  InvenTreePart part;
51

52
  InvenTreePart? parentPart;
53

54
  int parameterCount = 0;
55

56
  bool showParameters = false;
57
  bool showBom = false;
58

59
  int attachmentCount = 0;
60
  int bomCount = 0;
61
  int usedInCount = 0;
62
  int variantCount = 0;
63

64
  List<Map<String, dynamic>> labels = [];
65

66
  @override
×
67
  String getAppBarTitle() => L10().partDetails;
×
68

69
  @override
×
70
  List<Widget> appBarActions(BuildContext context) {
71
    List<Widget> actions = [];
×
72

73
    if (InvenTreePart().canEdit) {
×
74
      actions.add(
×
75
          IconButton(
×
76
              icon: Icon(TablerIcons.edit),
×
77
              tooltip: L10().editPart,
×
78
              onPressed: () {
×
79
                _editPartDialog(context);
×
80
              }
81
          )
82
      );
83
    }
84
    return actions;
85
  }
86

87
  @override
×
88
  List<SpeedDialChild> barcodeButtons(BuildContext context) {
89
    List<SpeedDialChild> actions = [];
×
90

91
    if (InvenTreePart().canEdit) {
×
92
      actions.add(
×
93
          customBarcodeAction(
×
94
              context, this,
95
              widget.part.customBarcode, "part",
×
96
              widget.part.pk
×
97
          )
98
      );
99
    }
100

101
    return actions;
102
  }
103

104
  @override
×
105
  List<SpeedDialChild> actionButtons(BuildContext context) {
106
    List<SpeedDialChild> actions = [];
×
107

108
    if (InvenTreeStockItem().canCreate) {
×
109
      actions.add(
×
110
          SpeedDialChild(
×
111
              child: Icon(TablerIcons.packages),
×
112
              label: L10().stockItemCreate,
×
113
              onTap: () {
×
114
                _newStockItem(context);
×
115
              }
116
          )
117
      );
118
    }
119

120
    if (labels.isNotEmpty) {
×
121
      actions.add(
×
122
        SpeedDialChild(
×
123
          child: Icon(TablerIcons.printer),
×
124
          label: L10().printLabel,
×
125
          onTap: () async {
×
126
            selectAndPrintLabel(
×
127
              context,
128
              labels,
×
129
              widget.part.pk,
×
130
              "part",
131
              "part=${widget.part.pk}"
×
132
            );
×
133
          }
134
        )
135
      );
136
    }
137

138
    return actions;
139
  }
140

141
  @override
×
142
  Future<void> onBuild(BuildContext context) async {
143
    refresh(context);
×
144

145
    if (mounted) {
×
146
      setState(() {});
×
147
    }
148
  }
149

150
  @override
×
151
  Future<void> request(BuildContext context) async {
152

153
    final bool result = await part.reload();
×
154

155
    if (!result || part.pk == -1) {
×
156
      // Part could not be loaded, for some reason
157
      Navigator.of(context).pop();
×
158
      return;
159
    }
160

161
    // If the part points to a parent "template" part, request that too
162
    int? templatePartId = part.variantOf;
×
163

164
    if (templatePartId == null) {
165
      parentPart = null;
×
166
    } else {
167
      final result = await InvenTreePart().get(templatePartId);
×
168

169
      if (result != null && result is InvenTreePart) {
×
170
        parentPart = result;
×
171
      } else {
172
        parentPart = null;
×
173
      }
174
    }
175

176
    // Request part test templates
177
    part.getTestTemplates().then((value) {
×
178
      if (mounted) {
×
179
        setState(() {});
×
180
      }
181
    });
182

183
    // Request the number of parameters for this part
184
    showParameters = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_PARAMETERS, true);
×
185

186
    // Request the number of attachments
187
    InvenTreePartAttachment().countAttachments(part.pk).then((int value) {
×
188
      if (mounted) {
×
189
        setState(() {
×
190
          attachmentCount = value;
×
191
        });
192
      }
193
    });
194

195
    showBom = await InvenTreeSettingsManager().getBool(INV_PART_SHOW_BOM, true);
×
196

197
    // Request the number of BOM items
198
    InvenTreePart().count(
×
199
      filters: {
×
200
        "in_bom_for": part.pk.toString(),
×
201
      }
202
    ).then((int value) {
×
203
      if (mounted) {
×
204
        setState(() {
×
205
          bomCount = value;
×
206
        });
207
      }
208
    });
209

210
    // Request number of "used in" parts
211
    InvenTreeBomItem().count(
×
212
      filters: {
×
213
        "uses": part.pk.toString(),
×
214
      }
215
    ).then((int value) {
×
216
      if (mounted) {
×
217
        setState(() {
×
218
          usedInCount = value;
×
219
        });
220
      }
221
    });
222

223
    // Request the number of variant items
224
    InvenTreePart().count(
×
225
      filters: {
×
226
        "variant_of": part.pk.toString(),
×
227
      }
228
    ).then((int value) {
×
229
      if (mounted) {
×
230
        setState(() {
×
231
          variantCount = value;
×
232
        });
233
      }
234
    });
235

236
    List<Map<String, dynamic>> _labels = [];
×
237
    bool allowLabelPrinting = await InvenTreeSettingsManager().getBool(INV_ENABLE_LABEL_PRINTING, true);
×
238
    allowLabelPrinting &= api.supportsMixin("labels");
×
239

240
    if (allowLabelPrinting) {
241

242
      String model_type = api.supportsModernLabelPrinting ? InvenTreePart.MODEL_TYPE : "part";
×
243
      String item_key = api.supportsModernLabelPrinting ? "items" : "part";
×
244

245
      _labels = await getLabelTemplates(
×
246
          model_type,
247
          {
×
248
            item_key: widget.part.pk.toString()
×
249
          }
250
      );
251
    }
252

253
    if (mounted) {
×
254
      setState(() {
×
255
        labels = _labels;
×
256
      });
257
    }
258
  }
259

260
  void _editPartDialog(BuildContext context) {
×
261

262
    part.editForm(
×
263
      context,
264
      L10().editPart,
×
265
      onSuccess: (data) async {
×
266
        refresh(context);
×
267
        showSnackIcon(L10().partEdited, success: true);
×
268
      }
269
    );
270
  }
271

272
  Widget headerTile() {
×
273
    return Card(
×
274
        child: ListTile(
×
275
          title: Text("${part.fullname}"),
×
276
          subtitle: Text("${part.description}"),
×
277
          trailing: Text(
×
278
            part.stockString(),
×
279
            style: TextStyle(
×
280
              fontSize: 20,
281
            )
282
          ),
283
          leading: GestureDetector(
×
284
            child: api.getImage(part.thumbnail),
×
285
            onTap: () {
×
286
              Navigator.push(
×
287
                context,
×
288
                MaterialPageRoute(
×
289
                  builder: (context) => PartImageWidget(part)
×
290
                )
291
              ).then((value) {
×
292
                refresh(context);
×
293
              });
294
            }),
295
        ),
296
    );
297
  }
298

299
  /*
300
   * Build a list of tiles to display under the part description
301
   */
302
  List<Widget> partTiles() {
×
303

304
    List<Widget> tiles = [];
×
305

306
    // Image / name / description
307
    tiles.add(
×
308
      headerTile()
×
309
    );
310

311
    if (loading) {
×
312
      tiles.add(progressIndicator());
×
313
      return tiles;
314
    }
315

316
    if (!part.isActive) {
×
317
      tiles.add(
×
318
        ListTile(
×
319
          title: Text(
×
320
              L10().inactive,
×
321
              style: TextStyle(
×
322
                color: COLOR_DANGER
323
              )
324
          ),
325
          subtitle: Text(
×
326
            L10().inactiveDetail,
×
327
            style: TextStyle(
×
328
              color: COLOR_DANGER
329
            )
330
          ),
331
          leading: Icon(
×
332
              TablerIcons.exclamation_circle,
333
              color: COLOR_DANGER
334
          ),
335
        )
336
      );
337
    }
338

339
    if (parentPart != null) {
×
340
      tiles.add(
×
341
        ListTile(
×
342
          title: Text(L10().templatePart),
×
343
          subtitle: Text(parentPart!.fullname),
×
344
          leading: api.getImage(
×
345
            parentPart!.thumbnail,
×
346
            width: 32,
347
            height: 32,
348
          ),
349
          onTap: () {
×
350
            Navigator.push(
×
351
              context,
×
352
              MaterialPageRoute(builder: (context) => PartDetailWidget(parentPart!))
×
353
            );
354
          }
355
        )
356
      );
357
    }
358

359
    // Category information
360
    if (part.categoryName.isNotEmpty) {
×
361
      tiles.add(
×
362
        ListTile(
×
363
            title: Text(L10().partCategory),
×
364
            subtitle: Text("${part.categoryName}"),
×
365
            leading: Icon(TablerIcons.sitemap, color: COLOR_ACTION),
×
366
            onTap: () async {
×
367
              if (part.categoryId > 0) {
×
368

369
                showLoadingOverlay();
×
370
                var cat = await InvenTreePartCategory().get(part.categoryId);
×
371
                hideLoadingOverlay();
×
372

373
                if (cat is InvenTreePartCategory) {
×
374
                  Navigator.push(context, MaterialPageRoute(
×
375
                      builder: (context) => CategoryDisplayWidget(cat)));
×
376
                }
377
              }
378
            },
379
          )
380
      );
381
    } else {
382
      tiles.add(
×
383
          ListTile(
×
384
            title: Text(L10().partCategory),
×
385
            subtitle: Text(L10().partCategoryTopLevel),
×
386
            leading: Icon(TablerIcons.sitemap, color: COLOR_ACTION),
×
387
            onTap: () {
×
388
              Navigator.push(context, MaterialPageRoute(
×
389
                  builder: (context) => CategoryDisplayWidget(null)));
×
390
            },
391
          )
392
      );
393
    }
394

395
    // Display number of "variant" parts if any exist
396
    if (variantCount > 0) {
×
397
      tiles.add(
×
398
          ListTile(
×
399
            title: Text(L10().variants),
×
400
            leading: Icon(TablerIcons.versions, color: COLOR_ACTION),
×
401
            trailing: Text(variantCount.toString()),
×
402
            onTap: () {
×
403
              Navigator.push(
×
404
                  context,
×
405
                  MaterialPageRoute(
×
406
                      builder: (context) => PartList(
×
407
                          {
×
408
                            "variant_of": part.pk.toString(),
×
409
                          },
410
                          title: L10().variants
×
411
                      )
412
                  )
413
              );
414
            },
415
          )
416
      );
417
    }
418

419
    tiles.add(
×
420
      ListTile(
×
421
        title: Text(L10().availableStock),
×
422
        subtitle: Text(L10().stockDetails),
×
423
        leading: Icon(TablerIcons.packages),
×
424
        trailing: Text(
×
425
          part.stockString(),
×
426
          style: TextStyle(
×
427
            fontWeight: FontWeight.bold,
428
          ),
429
        ),
430
      ),
431
    );
432

433
    // Tiles for "purchaseable" parts
434
    if (part.isPurchaseable) {
×
435

436
      // On order
437
      tiles.add(
×
438
        ListTile(
×
439
          title: Text(L10().onOrder),
×
440
          subtitle: Text(L10().onOrderDetails),
×
441
          leading: Icon(TablerIcons.shopping_cart),
×
442
          trailing: Text("${part.onOrderString}"),
×
443
          onTap: () {
×
444
            // TODO - Order views
445
          },
446
        )
447
      );
448

449
    }
450

451
    // Tiles for an "assembly" part
452
    if (part.isAssembly) {
×
453

454
      if (showBom && bomCount > 0) {
×
455
        tiles.add(
×
456
            ListTile(
×
457
                title: Text(L10().billOfMaterials),
×
458
                leading: Icon(TablerIcons.list_tree, color: COLOR_ACTION),
×
459
                trailing: Text(bomCount.toString()),
×
460
                onTap: () {
×
461
                  Navigator.push(context, MaterialPageRoute(
×
462
                      builder: (context) => BillOfMaterialsWidget(part, isParentComponent: true)
×
463
                  ));
464
                },
465
            )
466
        );
467
      }
468

469
      if (part.building > 0) {
×
470
        tiles.add(
×
471
            ListTile(
×
472
              title: Text(L10().building),
×
473
              leading: Icon(TablerIcons.tools),
×
474
              trailing: Text("${simpleNumberString(part.building)}"),
×
475
              onTap: () {
×
476
                // TODO
477
              },
478
            )
479
        );
480
      }
481
    }
482

483
    if (part.isComponent) {
×
484
      if (showBom && usedInCount > 0) {
×
485
        tiles.add(
×
486
          ListTile(
×
487
            title: Text(L10().usedIn),
×
488
            subtitle: Text(L10().usedInDetails),
×
489
            leading: Icon(TablerIcons.stack_2, color: COLOR_ACTION),
×
490
            trailing: Text(usedInCount.toString()),
×
491
              onTap: () {
×
492
                Navigator.push(
×
493
                    context,
×
494
                    MaterialPageRoute(
×
495
                        builder: (context) => BillOfMaterialsWidget(part, isParentComponent: false)
×
496
                    )
497
                );
498
              }
499
          )
500
        );
501
      }
502
    }
503

504
    // Keywords?
505
    if (part.keywords.isNotEmpty) {
×
506
      tiles.add(
×
507
          ListTile(
×
508
            title: Text("${part.keywords}"),
×
509
            leading: Icon(TablerIcons.tags),
×
510
          )
511
      );
512
    }
513

514
    // External link?
515
    if (part.link.isNotEmpty) {
×
516
      tiles.add(
×
517
          ListTile(
×
518
            title: Text("${part.link}"),
×
519
            leading: Icon(TablerIcons.link, color: COLOR_ACTION),
×
520
            onTap: () {
×
521
              part.openLink();
×
522
            },
523
          )
524
      );
525
    }
526

527
    // Tiles for "component" part
528
    if (part.isComponent && part.usedInCount > 0) {
×
529

530
      tiles.add(
×
531
        ListTile(
×
532
          title: Text(L10().usedIn),
×
533
          subtitle: Text(L10().usedInDetails),
×
534
          leading: Icon(TablerIcons.sitemap),
×
535
          trailing: Text("${part.usedInCount}"),
×
536
          onTap: () {
×
537
            // TODO
538
          },
539
        )
540
      );
541
    }
542

543
    if (part.isPurchaseable) {
×
544

545
      if (part.supplierCount > 0) {
×
546
        tiles.add(
×
547
            ListTile(
×
548
              title: Text(L10().suppliers),
×
549
              leading: Icon(TablerIcons.building_factory, color: COLOR_ACTION),
×
550
              trailing: Text("${part.supplierCount}"),
×
551
                onTap: () {
×
552
                  Navigator.push(
×
553
                    context,
×
554
                    MaterialPageRoute(builder: (context) => SupplierPartList({
×
555
                      "part": part.pk.toString()
×
556
                    }))
557
                  );
558
                },
559
            )
560
        );
561
      }
562
    }
563

564
    // Notes field
565
    tiles.add(
×
566
        ListTile(
×
567
          title: Text(L10().notes),
×
568
          leading: Icon(TablerIcons.note, color: COLOR_ACTION),
×
569
          trailing: Text(""),
×
570
          onTap: () {
×
571
            Navigator.push(
×
572
                context,
×
573
                MaterialPageRoute(builder: (context) => NotesWidget(part))
×
574
            );
575
          },
576
        )
577
    );
578

579
    tiles.add(
×
580
      ListTile(
×
581
        title: Text(L10().attachments),
×
582
        leading: Icon(TablerIcons.file, color: COLOR_ACTION),
×
583
        trailing: attachmentCount > 0 ? Text(attachmentCount.toString()) : null,
×
584
        onTap: () {
×
585
          Navigator.push(
×
586
            context,
×
587
            MaterialPageRoute(
×
588
              builder: (context) => AttachmentWidget(
×
589
                  InvenTreePartAttachment(),
×
590
                  part.pk,
×
591
                  L10().part,
×
592
                  part.canEdit
×
593
                )
594
            )
595
          );
596
        },
597
      )
598
    );
599

600
    return tiles;
601

602
  }
603

604
  // Return tiles for each stock item
605
  List<Widget> stockTiles() {
×
606
    List<Widget> tiles = [];
×
607

608
    tiles.add(headerTile());
×
609

610
    tiles.add(
×
611
      ListTile(
×
612
        title: Text(
×
613
          L10().stockItems,
×
614
          style: TextStyle(fontWeight: FontWeight.bold),
×
615
        ),
616
        subtitle: part.stockItems.isEmpty ? Text(L10().stockItemsNotAvailable) : null,
×
617
        trailing: part.stockItems.isNotEmpty ? Text("${part.stockItems.length}") : null,
×
618
      )
619
    );
620

621
    return tiles;
622
  }
623

624
  /*
625
   * Launch a form to create a new StockItem for this part
626
   */
627
  Future<void> _newStockItem(BuildContext context) async {
×
628

629
    var fields = InvenTreeStockItem().formFields();
×
630

631
    // Serial number cannot be directly edited here
632
    fields.remove("serial");
×
633

634
    // Hide the "part" field
635
    fields["part"]?["hidden"] = true;
×
636

637
    int? default_location = part.defaultLocation;
×
638

639
    Map<String, dynamic> data = {
×
640
      "part": part.pk.toString()
×
641
    };
642

643
    if (default_location != null) {
644
      data["location"] = default_location;
×
645
    }
646

647
    if (part.isTrackable) {
×
648
      // read the next available serial number
649
      showLoadingOverlay();
×
650
      var response = await api.get("/api/part/${part.pk}/serial-numbers/", expectedStatusCode: null);
×
651
      hideLoadingOverlay();
×
652

653
      if (response.isValid() && response.statusCode == 200) {
×
654
        data["serial_numbers"] = response.data["next"] ?? response.data["latest"];
×
655
      }
656

657
      print("response: " + response.statusCode.toString() + response.data.toString());
×
658

659
    } else {
660
      // Cannot set serial numbers for non-trackable parts
661
      fields.remove("serial_numbers");
×
662
    }
663

664
    print("data: ${data.toString()}");
×
665

666
    InvenTreeStockItem().createForm(
×
667
        context,
668
        L10().stockItemCreate,
×
669
        fields: fields,
670
        data: data,
671
        onSuccess: (result) async {
×
672

673
          Map<String, dynamic> data = result as Map<String, dynamic>;
674

675
          if (data.containsKey("pk")) {
×
676
            var item = InvenTreeStockItem.fromJson(data);
×
677

678
            Navigator.push(
×
679
                context,
680
                MaterialPageRoute(
×
681
                    builder: (context) => StockDetailWidget(item)
×
682
                )
683
            );
684
          }
685
        }
686
    );
687
  }
688

689
  @override
×
690
  List<Widget> getTabIcons(BuildContext context) {
691
    List<Widget> icons = [
×
692
      Tab(text: L10().details),
×
693
      Tab(text: L10().stock)
×
694
    ];
695

696
    if (showParameters) {
×
697
      icons.add(Tab(text: L10().parameters));
×
698
    }
699

700
    return icons;
701
  }
702

703
  @override
×
704
  List<Widget> getTabs(BuildContext context) {
705
    List<Widget> tabs = [
×
NEW
706
      SingleChildScrollView(
×
NEW
707
        physics: AlwaysScrollableScrollPhysics(),
×
NEW
708
        child: Column(
×
NEW
709
          children: partTiles(),
×
710
        )
711
      ),
712
      PaginatedStockItemList({"part": part.pk.toString()})
×
713
    ];
714

715
    if (showParameters) {
×
716
      tabs.add(PaginatedParameterList({"part": part.pk.toString()}));
×
717
    }
718

719
    return tabs;
720
  }
721

722
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc