我看到有几家公司在复制粘贴销售订单行时遇到困难,尤其是订单量大且供应链管理和定价逻辑复杂时。根据你设置D365的方式,你会看到粘贴销售线每行可能花费1.5秒,甚至超过6秒。而且还有更糟糕的例子。
我真的研究过所有代码和SQL语句,发生的事情非常多。
所以我问自己,用人工智能我能做得更好吗?我应该试试用“Vibe编码”几分钟吗?
我的想法是不粘贴到网格里,而是粘贴到文本字段里,然后让一个类在同一个TTS内创建销售线。
所以我向ChatGPT提出了请求。经过几次迭代,它实际上创建了正是这样一个类。我只需要添加一个菜单项,然后添加到销售线表单。
这是它提出的解决方案:
我们在销售订单表单上有一个“快速粘贴”按钮,可以弹出对话框,我们可以粘贴商品[tab]Qty。我还让他做了一个估算器,显示完成100行需要多长时间。

接下来,点击“确定”,25 分钟后,我收到一笔有 100 行的销售订单:

在UDE沙盒中245毫秒的好速度还不错。在生产系统中,我希望每条销售线的帧率能进一步降低到100毫秒。
由于这是“Vibe编码”,代码尚未达到生产环境,应视为Alfa预览版。有很多改进的可能性,如果有人在上面创建了一个Github项目,我们可以为那些讨厌等待复制粘贴的人做点什么。

Here are the code for this demo:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
|
/// Action menu item: Class = SalesOrderQuickPasteLines/// Paste rows: TAB-separated (Excel). Extra columns ignored.////// Formats:/// 1) ItemId/// 2) ItemIdQty/// 3) ItemIdQty{PriceOrUnit}/// 4) ItemIdQty{PriceOrUnit}{PriceOrUnit}////// Rules:/// - Qty defaults to 1 if omitted/invalid./// - Price vs Unit ambiguity (col3/col4):/// * If value matches a Unit for the item => treat as SalesUnit/// * Else if numeric => treat as Price/// * Else ignore/// - If both are supplied across col3/col4, both are applied./// - >4 columns ignored.////// Behavior:/// - Insert in ONE TTS; abort on first failing row (ttsAbort + row/why)./// - st.GUPDelayPricingCalculation=Yes during insert TTS; restored before commit./// - sl.SkipCreateMarkup=Yes on inserted lines; cleared OUTSIDE TTS before retail recalc./// - Retail recalc OUTSIDE TTS: MCRSalesTableController::recalculateRetailPricesDiscounts(st)/// - Refresh SalesLine grid via caller datasource executeQuery.////// UI:/// - Single-column dialog layout (no side-by-side groups)/// - Instructions + Estimate as static text (no boxes)/// - Large multiline Notes paste box/// - Estimate updates immediately on paste/typingclass SalesOrderQuickPasteLines extends RunBase{ #define.ProgressEvery(20) #define.MaxErr(4000) #define.MsPerLine(250) #define.AggregateSameItemId(false) SalesId salesId; FormRun callerFr; str pasteText; DialogGroup gIntro, gLines; // Static texts (no boxes) DialogText dtHelp, dtEst; // Paste field DialogField dfLines; // Controls we touch at runtime FormStringControl cLines; public static void main(Args _a) { SalesTable st = _a ? _a.record() : null; if (!st || st.TableId != tableNum(SalesTable)) throw error("Run from SalesTable."); SalesOrderQuickPasteLines o = new SalesOrderQuickPasteLines(); o.parmSalesId(st.SalesId); o.parmCaller(_a ? _a.caller() as FormRun : null); if (o.prompt()) o.runOperation(); } public boolean canRunInNewSession() { return false; } public SalesId parmSalesId(SalesId _v = salesId) { salesId = _v; return salesId; } public FormRun parmCaller(FormRun _v = callerFr) { callerFr = _v; return callerFr; } // ---------------- Dialog ---------------- public Object dialog() { Dialog d = super(); d.caption("Paste lines (ItemIdQty)"); gIntro = d.addGroup("Instructions"); gIntro.columns(1); dtHelp = d.addText( "Paste TAB-separated rows from Excel:\n" + " ItemId\n" + " ItemIdQty\n" + " ItemIdQtyPriceOrUnit\n" + " ItemIdQtyPriceOrUnitPriceOrUnit\n" + "Col3/Col4: if it matches a Unit for the item -> Unit; else if numeric -> Price; else ignored." ); dtEst = d.addText("Lines: 0 | Est. time: 0 s (250 ms/line)"); // Critical: nested group prevents two-column layout gLines = d.addGroup("Lines", gIntro); gLines.columns(1); // Notes + ignore EDT constraints to avoid truncation dfLines = d.addField(extendedTypeStr(Notes), "Paste here", "", true); dfLines.value(""); dfLines.displayLength(200); dfLines.displayHeight(28); return d; } public void dialogPostRun(DialogRunbase _d) { super(_d); cLines = dfLines.control() as FormStringControl; if (cLines) { // Update estimate immediately on paste/typing (not only on focus leave) cLines.registerOverrideMethod( methodStr(FormStringControl, textChange), methodStr(SalesOrderQuickPasteLines, lines_textChange), this); } this.updateEstimate(cLines ? cLines.text() : ""); } public void lines_textChange(FormStringControl _ctrl) { this.updateEstimate(_ctrl ? _ctrl.text() : ""); } public boolean getFromDialog() { boolean ok = super(); pasteText = strLRTrim(dfLines.value()); return ok; } // ---------------- Execution ---------------- public void run() { if (!pasteText) return; // parse returns [lineNo,itemId,qty,c3,c4] List rows = this.parse(pasteText); int inputCount = rows.elements(); if (!inputCount) throw error("No valid rows. Expected at least ItemId per line."); int64 t0 = WinAPIServer::getTickCount(); int created = this.insertLinesInOneTts(rows, inputCount); int64 insertMs = WinAPIServer::getTickCount() - t0; int64 t1 = WinAPIServer::getTickCount(); this.postCommitRetailRecalc(); int64 recalcMs = WinAPIServer::getTickCount() - t1; this.refreshSalesLineDs(); Box::info( strFmt("Import completed.\nLines created: %1\nInsert time: %2 ms\nRecalc time: %3 ms.", created, insertMs, recalcMs), "QuickPaste"); } // ---------------- Live ETA ---------------- private void updateEstimate(str _text) { int n = this.estimateLineCount(_text); int64 ms = n * #MsPerLine; int sec = any2int((ms + 999) / 1000); str s = strFmt("Lines: %1 | Est. time: %2 s (%3 ms/line)", n, sec, #MsPerLine); if (dtEst) dtEst.text(s); } private int estimateLineCount(str _text) { if (!_text) return 0; _text = strReplace(_text, "\r", ""); List raw = Global::strSplit(_text, "\n"); ListEnumerator e = raw.getEnumerator(); int n = 0; while (e.moveNext()) { if (strLRTrim(e.current())) n++; } return n; } // ---------------- Unit/Price helpers ---------------- private boolean tryParseReal(str _s, real _out) { _s = strLRTrim(_s); if (!_s) { _out = 0.0; return false; } try { _out = any2real(_s); return true; } catch { _out = 0.0; return false; } } private boolean unitExistsForItem(InventTable _it, SalesUnit _unit) { if (!_unit) return false; if (_it && _it.salesUnitId() == _unit) return true; try { if (UnitOfMeasure::findBySymbol(_unit).RecId) return true; } catch { } return false; } // ---------------- Parsing ---------------- // Returns containers: [lineNo, itemId, qty, c3, c4] private List parse(str _in) { List lines = new List(Types::Container); if (!_in) return lines; _in = strReplace(_in, "\r", ""); List raw = Global::strSplit(_in, "\n"); ListEnumerator e = raw.getEnumerator(); int ln = 0; while (e.moveNext()) { ln++; str row = strLRTrim(e.current()); if (!row) continue; List cols = Global::strSplit(row, "\t"); ListEnumerator ce = cols.getEnumerator(); int col = 0; str itemStr = "", qtyStr = "", c3 = "", c4 = ""; while (ce.moveNext()) { col++; str v = strLRTrim(ce.current()); if (col == 1) itemStr = v; else if (col == 2) qtyStr = v; else if (col == 3) c3 = v; else if (col == 4) c4 = v; else break; } if (!itemStr) continue; ItemId itemId = itemStr; Qty qty = 1; if (qtyStr) { real rQty; if (this.tryParseReal(qtyStr, rQty) && rQty > 0) qty = rQty; } if (qty InventTable // Resolve to: [lineNo,itemId,qty,price,SalesUnit] List work = new List(Types::Container); ListEnumerator pe = _rows.getEnumerator(); while (pe.moveNext()) { container pc = pe.current(); int lineNo = conPeek(pc, 1); ItemId itemId = conPeek(pc, 2); Qty qty = conPeek(pc, 3); str c3 = conPeek(pc, 4); str c4 = conPeek(pc, 5); InventTable it; if (inv.exists(itemId)) it = inv.lookup(itemId); else { it = InventTable::find(itemId, true); if (!it.RecId) throw error(this.err(lineNo, itemId, qty, "Item does not exist.")); inv.insert(itemId, it); } Price price = 0; SalesUnit unit = ""; real r; if (c3) { SalesUnit u3 = c3; if (this.unitExistsForItem(it, u3)) unit = u3; else if (this.tryParseReal(c3, r)) price = r; } if (c4) { SalesUnit u4 = c4; if (!unit && this.unitExistsForItem(it, u4)) unit = u4; else if (!price && this.tryParseReal(c4, r)) price = r; } work.addEnd([lineNo, itemId, qty, price, unit]); } // Optional aggregation by item+unit+price only if (#AggregateSameItemId) { Map keyQty = new Map(Types::String, Types::Real); Map keyLine = new Map(Types::String, Types::Integer); ListEnumerator ae = work.getEnumerator(); while (ae.moveNext()) { container c = ae.current(); int lineNo = conPeek(c, 1); ItemId itemId = conPeek(c, 2); Qty qty = conPeek(c, 3); Price price = conPeek(c, 4); SalesUnit unit = conPeek(c, 5); str key = strFmt("%1|%2|%3", itemId, unit, price); if (!keyLine.exists(key)) keyLine.insert(key, lineNo); real prev = keyQty.exists(key) ? keyQty.lookup(key) : 0.0; keyQty.insert(key, prev + qty); } work = new List(Types::Container); MapEnumerator me = keyQty.getEnumerator(); while (me.moveNext()) { str key = me.currentKey(); real qtySum = me.currentValue(); List parts = Global::strSplit(key, "|"); ListEnumerator le = parts.getEnumerator(); ItemId itemId; SalesUnit unit; Price price; int idx = 0; while (le.moveNext()) { idx++; str v = le.current(); if (idx == 1) itemId = v; else if (idx == 2) unit = v; else if (idx == 3) price = any2real(v); } int lineNo = keyLine.lookup(key); work.addEnd([lineNo, itemId, qtySum, price, unit]); } } int progressCounter = 0; int64 t0 = WinAPIServer::getTickCount(); int workTotal = work.elements(); ttsBegin; try { origDelay = st.GUPDelayPricingCalculation; st.GUPDelayPricingCalculation = NoYes::Yes; st.doUpdate(); ListEnumerator e2 = work.getEnumerator(); while (e2.moveNext()) { container c2 = e2.current(); int lineNo = conPeek(c2, 1); ItemId itemId = conPeek(c2, 2); Qty qty = conPeek(c2, 3); Price price = conPeek(c2, 4); SalesUnit unit = conPeek(c2, 5); InventTable it = inv.lookup(itemId); this.createFast(st, it, qty, price, unit, lineNo); created++; progressCounter++; if (progressCounter >= #ProgressEvery || created == workTotal) { progressCounter = 0; int64 elapsed = WinAPIServer::getTickCount() - t0; real avgMs = created ? (elapsed / created) : 0; real remMs = avgMs * (workTotal - created); p.setText(strFmt("Created %1/%2. Est. remaining: %3 s", created, workTotal, any2int(remMs / 1000))); } p.incCount(1); } st = SalesTable::find(this.salesId, true); st.GUPDelayPricingCalculation = origDelay; st.doUpdate(); ttsCommit; } catch (Exception::Error) { ttsAbort; throw; } return created; } private void createFast(SalesTable _st, InventTable _it, Qty _qty, Price _price, SalesUnit _unit, int _lineNo) { try { SalesLine sl; sl.clear(); sl.initValue(); sl.SalesId = _st.SalesId; sl.ItemId = _it.ItemId; sl.SalesQty = _qty; sl.SalesUnit = _unit ? _unit : _it.salesUnitId(); if (_price && _price > 0) { sl.SalesPrice = _price; sl.PriceUnit = 1; } sl.SkipCreateMarkup = NoYes::Yes; sl.createLine(false, true, true, false, false, false); } catch (Exception::Error) { throw error(this.err(_lineNo, _it.ItemId, _qty, this.lastInfo(#MaxErr))); } } // ---------------- Post-commit retail recalc OUTSIDE TTS ---------------- private void postCommitRetailRecalc() { SalesTable st = SalesTable::find(this.salesId, true); SalesLine salesLine; update_recordset salesLine setting SkipCreateMarkup = NoYes::No where salesLine.SalesId == st.SalesId && salesLine.SkipCreateMarkup == NoYes::Yes; MCRSalesTableController::recalculateRetailPricesDiscounts(st); } private void refreshSalesLineDs() { if (!callerFr) return; FormDataSource ds = callerFr.dataSource(formDataSourceStr(SalesTable, SalesLine)); if (ds) ds.executeQuery(); } private str err(int _n, ItemId _i, Qty _q, str _r) { _r = strLRTrim(_r); if (!_r) _r = "Unknown error."; return strFmt("Import failed. Input line %1 (ItemId=%2, Qty=%3). Reason: %4", _n, _i, _q, _r); } private str lastInfo(int _max) { str t = ""; try { for (int i = infolog.num(); i > 0 && strLen(t) _max) t = substr(t, 1, _max); return t; }} |
转载请注明:ww12345678 的部落格 | AX Helper » D365:销售行复制粘贴速度为每行<250毫秒——Vibe代码