2031 lines
111 KiB
C#
2031 lines
111 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Diagnostics;
|
||
using System.Drawing;
|
||
using System.IO;
|
||
using System.Windows.Forms;
|
||
using Microsoft.Win32;
|
||
using Smart.CustomComponents;
|
||
|
||
namespace 成套报价软件
|
||
{
|
||
/// <summary>主窗口 拆分文件:由 主窗口.cs 按模块拆出,同一 partial class。</summary>
|
||
public partial class MainForm
|
||
{
|
||
// ============================================================
|
||
// 公式实时重算(对应 Excel 的三级联动公式)
|
||
// 元件(底层,每行独立) → 箱号(聚合该箱号所有元件) → 图号(聚合该图号所有箱号)
|
||
// ============================================================
|
||
|
||
/// <summary>把单元格值解析为 decimal;DBNull/空串/非数字返回 0。</summary>
|
||
private static decimal ParseDecimal(object v)
|
||
{
|
||
if (v == null || v == DBNull.Value) return 0m;
|
||
decimal d;
|
||
if (decimal.TryParse(v.ToString().Trim(), out d)) return d;
|
||
return 0m;
|
||
}
|
||
|
||
/// <summary>把计算结果写回到行某列;为 0 写空串(保持界面整洁)。
|
||
/// ★ 虚拟模式:写内存 DataTable 行(按行号)+刷新该行,不再写 Cells.Value(虚拟模式下无效)。</summary>
|
||
private void SetCell(DataGridViewRow row, string colName, decimal value)
|
||
{
|
||
if (row == null || row.DataGridView == null) return;
|
||
DataGridViewColumn col = FindColumn(row.DataGridView, colName);
|
||
if (col == null) return;
|
||
// 写 double(ValueType=double,Format 才能格式化显示)
|
||
object v = value == 0m ? 0d : (double)value;
|
||
string realName = !string.IsNullOrEmpty(col.DataPropertyName) ? col.DataPropertyName : colName;
|
||
写单元格(row.DataGridView, row.Index, realName, v);
|
||
}
|
||
|
||
// ============================================================
|
||
// 价格显示格式化(用 DefaultCellStyle.Format,只改显示不改值)
|
||
// - 这是 DataGridView 原生的“只改显示”机制,不干扰用户输入
|
||
// - 仅作用于公式计算列(见 s_公式列集合);用户输入列(表价/数量/系数)不参与
|
||
// ============================================================
|
||
|
||
// ============================================================
|
||
// ★ 虚拟模式供数/回写:表格不存数据,由内存 DataTable 按需提供
|
||
// ============================================================
|
||
|
||
/// <summary>虚拟模式:控件要显示某单元格时从内存表取值。</summary>
|
||
private void Dgv_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
|
||
{
|
||
var dgv = sender as DataGridView;
|
||
if (dgv == null) return;
|
||
var table = 取dgv数据表(dgv);
|
||
if (table == null || e.RowIndex < 0 || e.RowIndex >= table.Rows.Count) { e.Value = ""; return; }
|
||
if (e.ColumnIndex < 0 || e.ColumnIndex >= dgv.Columns.Count) { e.Value = ""; return; }
|
||
string colName = dgv.Columns[e.ColumnIndex].DataPropertyName;
|
||
if (string.IsNullOrEmpty(colName) || !table.Columns.Contains(colName)) { e.Value = ""; return; }
|
||
|
||
// ★ 合并视图序号显示(2026-09-30 多选联动):
|
||
// 箱号合并(元件表):显示"箱号-N"(如 AH4-1),用户可分辨每行归属(仅显示层,不写库)
|
||
// 图号合并(箱号表):显示连续行号 1..N
|
||
if (colName == "序号" &&
|
||
string.IsNullOrEmpty(table.TableName) && table.Columns.Contains("_来源表名_"))
|
||
{
|
||
if (table.Columns.Contains("_归属箱号_") && table.Columns.Contains("_段内序号_"))
|
||
{
|
||
string xhName = Convert.ToString(table.Rows[e.RowIndex]["_归属箱号_"]);
|
||
int seqIn = 0;
|
||
try { seqIn = Convert.ToInt32(table.Rows[e.RowIndex]["_段内序号_"]); } catch { }
|
||
e.Value = (string.IsNullOrWhiteSpace(xhName) ? "" : xhName + "-") + seqIn.ToString();
|
||
}
|
||
else
|
||
{
|
||
e.Value = (e.RowIndex + 1).ToString();
|
||
}
|
||
return;
|
||
}
|
||
|
||
object v = table.Rows[e.RowIndex][colName];
|
||
// 公式列输出数值类型(DefaultCellStyle.Format 只对数值生效)
|
||
if (v != null && v != DBNull.Value && v is string &&
|
||
s_公式列集合.Contains(colName))
|
||
{
|
||
double d;
|
||
string s = v.ToString().Trim();
|
||
if (s.Length > 0 && double.TryParse(s,
|
||
System.Globalization.NumberStyles.Float,
|
||
System.Globalization.CultureInfo.InvariantCulture, out d))
|
||
{ e.Value = d; return; }
|
||
if (s.Length == 0) { e.Value = 0d; return; }
|
||
}
|
||
e.Value = (v == null || v == DBNull.Value) ? "" : v;
|
||
}
|
||
|
||
/// <summary>虚拟模式:用户编辑单元格提交时写回内存表(读缓存),并排队直写数据库。</summary>
|
||
private void Dgv_CellValuePushed(object sender, DataGridViewCellValueEventArgs e)
|
||
{
|
||
var dgv = sender as DataGridView;
|
||
if (dgv == null) return;
|
||
var table = 取dgv数据表(dgv);
|
||
if (table == null || e.RowIndex < 0 || e.RowIndex >= table.Rows.Count) return;
|
||
if (e.ColumnIndex < 0 || e.ColumnIndex >= dgv.Columns.Count) return;
|
||
string colName = dgv.Columns[e.ColumnIndex].DataPropertyName;
|
||
if (string.IsNullOrEmpty(colName) || !table.Columns.Contains(colName)) return;
|
||
object 新值 = e.Value;
|
||
// ★ 图号/箱号重名自动加后缀:改成同表已有名时自动变"名(2)"/"名(3)"(不弹窗不打断;
|
||
// 元件名称不查重——元件名本来就常重复)。后缀版写回内存表并刷新该行显示。
|
||
if ((dgv == this.advancedDataGridView9 && colName == "图号") ||
|
||
(dgv == this.advancedDataGridView4 && colName == "箱号"))
|
||
{
|
||
新值 = 重名自动后缀(table, e.RowIndex, colName, 新值);
|
||
}
|
||
table.Rows[e.RowIndex][colName] = (新值 == null || 新值 == DBNull.Value) ? DBNull.Value : 新值;
|
||
// ★ 直接操作数据库:编辑即排队 UPDATE 库(防抖)
|
||
入队直写库(dgv, e.RowIndex, colName, 新值);
|
||
if (!Equals(新值, e.Value))
|
||
{
|
||
// 后缀生效:刷新该行显示(虚拟模式整表刷新一次供数)
|
||
try { dgv.InvalidateRow(e.RowIndex); } catch { }
|
||
Log("[重名] " + colName + " 重名自动改名: " + e.Value + " → " + 新值);
|
||
}
|
||
|
||
// ★★ 一劳永逸方案(2026-09-30 定稿):编辑提交后【只算本行元件公式】,
|
||
// 箱号行/图号行的聚合价格【一律不在编辑时更新内存显示】——
|
||
// 统一交给"防抖全库重算"(编辑停止 600ms 后):
|
||
// RecalcAllFromDatabase 按箱号逐个聚合 UPDATE 回库(库内计算,天然按归属) →
|
||
// 刷新箱号表显示从库(按内部ID逐行对齐) 一次跳到最终正确值。
|
||
// 单一更新路径 + 归属定位,结构上杜绝:①价格跳好几次中间值 ②显示到别的箱号。
|
||
// (旧的多条内存聚合/回填路径全部废弃——它们是跳动与错位的来源)
|
||
_isRecalculating = true;
|
||
_isReloading = true;
|
||
try
|
||
{
|
||
if (dgv == this.advancedDataGridView5)
|
||
{
|
||
// 本行元件公式(采购单价/采购价/报出单价/报出价)——单行数据自洽,即时显示
|
||
var yjRow = dgv.Rows[e.RowIndex];
|
||
if (yjRow != null && !yjRow.IsNewRow) RecalcYuanjianRow(yjRow);
|
||
|
||
// ★ B2(2026-10-01):记录本次编辑影响到的元件物理表(合并视图=行级来源表,
|
||
// 单箱号=整表表名),防抖到点后反查(图号,箱号)做增量重算——大项目编辑不再全库重算
|
||
try
|
||
{
|
||
bool mergedYj = string.IsNullOrEmpty(table.TableName) && table.Columns.Contains("_来源表名_");
|
||
string srcTbl = (mergedYj
|
||
? Convert.ToString(table.Rows[e.RowIndex]["_来源表名_"])
|
||
: table.TableName) ?? "";
|
||
srcTbl = srcTbl.Trim().Trim('"');
|
||
if (!string.IsNullOrWhiteSpace(srcTbl))
|
||
{
|
||
lock (_待重算元件表) { _待重算元件表.Add(srcTbl); }
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
// 箱号表(dgv4)/图号表(dgv9)编辑:自身派生列依赖下级聚合,同样交给防抖重算
|
||
// (dgv4/dgv9 不收集范围→防抖到点走全量,安全优先)
|
||
// ★ 清缓存:切回单选/切图号时 Reload 读库新值
|
||
_yuanjianCache.Clear();
|
||
_xianghaoCache.Clear();
|
||
合并视图防抖重算();
|
||
}
|
||
finally
|
||
{
|
||
_isRecalculating = false;
|
||
_isReloading = false;
|
||
}
|
||
|
||
// ★ 项目页实时同步(800ms 防抖):图号汇总→项目列表库+项目页网格刷新
|
||
if (dgv == this.advancedDataGridView5 || dgv == this.advancedDataGridView4 || dgv == this.advancedDataGridView9)
|
||
启动项目页同步();
|
||
}
|
||
|
||
/// <summary>★ 合并视图编辑后的防抖全库重算(2026-09-30):
|
||
/// 连续编辑合并触发一次(600ms 安静期),RecalcAllFromDatabase 纯库操作——
|
||
/// 按图号/箱号逐个聚合 UPDATE 回库,保证箱号行/图号行/项目汇总始终最新,
|
||
/// 且不会把"多箱号合计"写进单个箱号行。
|
||
/// ★ B2(2026-10-01):编辑收集过影响元件表时改为【增量重算】(只重算受影响箱号),
|
||
/// 未收集(dgv4/dgv9编辑等)走全量兜底。</summary>
|
||
private Timer _合并重算Timer;
|
||
/// <summary>B2:编辑影响到的元件物理表集合(防抖窗口内合并),Tick 时取走并反查(图号,箱号)。</summary>
|
||
private readonly System.Collections.Generic.HashSet<string> _待重算元件表 =
|
||
new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
|
||
|
||
/// <summary>B2:元件物理表名集合 → 反查(图号\n箱号内部ID)范围集合。</summary>
|
||
private System.Collections.Generic.HashSet<string> 反查重算范围(System.Collections.Generic.HashSet<string> 表名s)
|
||
{
|
||
var scope = new System.Collections.Generic.HashSet<string>();
|
||
if (表名s == null || 表名s.Count == 0 || string.IsNullOrWhiteSpace(_currentProjectId)) return scope;
|
||
string dbPath = GetQuoteDbPath(_currentProjectId);
|
||
if (!System.IO.File.Exists(dbPath)) return scope;
|
||
try
|
||
{
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
foreach (string t in 表名s)
|
||
{
|
||
using (var sel = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 图号, 箱号 FROM 表索引 WHERE 类型='元件' AND 实际表名=@t;", conn))
|
||
{
|
||
sel.Parameters.AddWithValue("@t", t);
|
||
using (var rd = sel.ExecuteReader())
|
||
{
|
||
while (rd.Read())
|
||
{
|
||
string th = rd.IsDBNull(0) ? "" : rd.GetValue(0).ToString().Trim();
|
||
string xh = rd.IsDBNull(1) ? "" : rd.GetValue(1).ToString().Trim();
|
||
if (!string.IsNullOrWhiteSpace(th) && !string.IsNullOrWhiteSpace(xh))
|
||
scope.Add(th + "\n" + xh);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (Exception exQ) { Log("[重算·增量] 反查范围异常: " + exQ.Message); }
|
||
return scope;
|
||
}
|
||
|
||
private void 合并视图防抖重算()
|
||
{
|
||
if (_合并重算Timer == null)
|
||
{
|
||
_合并重算Timer = new Timer();
|
||
_合并重算Timer.Interval = 600;
|
||
_合并重算Timer.Tick += (s, e) =>
|
||
{
|
||
_合并重算Timer.Stop();
|
||
try
|
||
{
|
||
// ★ 修复(2026-10-01):重算读库前先把直写队列强制落库——
|
||
// 避免直写(300ms)与重算(600ms)时序竞争导致重算读到旧值
|
||
直写队列强制落库();
|
||
// ★ B2:先取走编辑收集的影响表 → 反查范围;非空走增量,空走全量
|
||
string[] tbls;
|
||
lock (_待重算元件表)
|
||
{
|
||
tbls = new string[_待重算元件表.Count];
|
||
_待重算元件表.CopyTo(tbls);
|
||
_待重算元件表.Clear();
|
||
}
|
||
var scope = 反查重算范围(
|
||
new System.Collections.Generic.HashSet<string>(tbls));
|
||
RecalcAllFromDatabase(_currentProjectId,
|
||
scope.Count > 0 ? scope : null);
|
||
// ★ 显示保险(2026-09-30):库聚合完成后,按内部ID对齐从库刷新箱号表
|
||
// 价格列显示(不动行序/选中)——库是权威,彻底杜绝"改AH4显示到AH3"的错位
|
||
刷新箱号表显示从库();
|
||
// ★ 费用行实时联动(2026-10-01):百分比费用的表价已按最新基数在库里重写,
|
||
// 这里把元件表中费用行的显示值同步为新库值(普通行不动,不打断编辑)
|
||
刷新元件表费用行显示从库();
|
||
}
|
||
catch (Exception exR) { Log("[合并重算] 异常: " + exR.Message); }
|
||
};
|
||
}
|
||
_合并重算Timer.Stop();
|
||
_合并重算Timer.Start();
|
||
}
|
||
|
||
/// <summary>★ 从库刷新箱号表/图号表价格列显示(2026-09-30):按"内部ID"对齐更新内存行,
|
||
/// 不动行序与选中。合并视图(TableName 空)时跳过——由切回单选时重载。</summary>
|
||
private void 刷新箱号表显示从库()
|
||
{
|
||
try
|
||
{
|
||
刷新表价格列从库(this.advancedDataGridView4, _xianghaoTable,
|
||
"单台成本, 成本价, 单台报出, 报出价, 利润, 利润率, 毛利率");
|
||
刷新表价格列从库(this.advancedDataGridView9, _tuhaoTable,
|
||
"箱柜数量, 单价, 总价, 成本总价, 毛利, 毛利率");
|
||
}
|
||
catch (Exception exF) { Log("[显示同步] 刷新异常: " + exF.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// ★ 费用行实时联动的显示同步(2026-10-01):重算后库里的费用行表价/四价已按最新基数重写,
|
||
/// 这里按内部ID对齐把元件表(内存表)中 类型='费用' 的行同步为新库值(普通行不动,不打断编辑)。
|
||
/// 合并视图按各行 _来源表名_ 读对应物理表;单箱号视图读整表。
|
||
/// </summary>
|
||
private void 刷新元件表费用行显示从库()
|
||
{
|
||
try
|
||
{
|
||
var tbl = _yuanjianTable;
|
||
var dgv = this.advancedDataGridView5;
|
||
if (dgv == null || tbl == null || string.IsNullOrWhiteSpace(_currentProjectId)) return;
|
||
if (!tbl.Columns.Contains("内部ID") || !tbl.Columns.Contains("类型")) return;
|
||
string dbPath = GetQuoteDbPath(_currentProjectId);
|
||
if (!System.IO.File.Exists(dbPath)) return;
|
||
|
||
// 内存表费用行(按内部ID)
|
||
var 费用行s = new System.Collections.Generic.List<DataRow>();
|
||
foreach (DataRow r in tbl.Rows)
|
||
if (Convert.ToString(r["类型"]).Trim() == "费用") 费用行s.Add(r);
|
||
if (费用行s.Count == 0) return;
|
||
|
||
bool merged = string.IsNullOrEmpty(tbl.TableName) && tbl.Columns.Contains("_来源表名_");
|
||
// 来源物理表集合(合并=行级来源;单箱号=整表)
|
||
var srcTables = new System.Collections.Generic.List<string>();
|
||
if (merged)
|
||
{
|
||
foreach (DataRow r in tbl.Rows)
|
||
{
|
||
string s = Convert.ToString(r["_来源表名_"]).Trim();
|
||
if (!string.IsNullOrWhiteSpace(s) && !srcTables.Contains(s)) srcTables.Add(s);
|
||
}
|
||
}
|
||
else if (!string.IsNullOrWhiteSpace(tbl.TableName))
|
||
{
|
||
srcTables.Add(tbl.TableName);
|
||
}
|
||
if (srcTables.Count == 0) return;
|
||
|
||
// 库值:内部ID → [备注,规格,表价,采购单价,采购价,报出单价,报出价]
|
||
var 库值 = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<object>>();
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
foreach (string s in srcTables)
|
||
{
|
||
try
|
||
{
|
||
using (var sel = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 内部ID, 备注, 规格, 表价, 采购单价, 采购价, 报出单价, 报出价 FROM \"" + s.Replace("\"", "") +
|
||
"\" WHERE 类型='费用';", conn))
|
||
using (var r = sel.ExecuteReader())
|
||
{
|
||
while (r.Read())
|
||
{
|
||
string nid = r.IsDBNull(0) ? "" : r.GetValue(0).ToString().Trim();
|
||
if (string.IsNullOrEmpty(nid)) continue;
|
||
var vals = new System.Collections.Generic.List<object>();
|
||
for (int i = 1; i < r.FieldCount; i++)
|
||
vals.Add(r.IsDBNull(i) ? (object)DBNull.Value : r.GetValue(i));
|
||
库值[nid] = vals;
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
|
||
// 对齐写回内存行(公式列保持 double 类型;规格/备注串同步——旧格式自愈后显示干净)
|
||
bool any = false;
|
||
string[] 列s = { "备注", "规格", "表价", "采购单价", "采购价", "报出单价", "报出价" };
|
||
foreach (DataRow row in 费用行s)
|
||
{
|
||
System.Collections.Generic.List<object> v;
|
||
string nid = Convert.ToString(row["内部ID"]).Trim();
|
||
if (string.IsNullOrEmpty(nid) || !库值.TryGetValue(nid, out v)) continue;
|
||
for (int i = 0; i < 列s.Length; i++)
|
||
{
|
||
if (!tbl.Columns.Contains(列s[i])) continue;
|
||
object 新值 = v[i];
|
||
object 旧值 = row[列s[i]] == DBNull.Value ? (object)"" : row[列s[i]];
|
||
if (新值 == DBNull.Value)
|
||
{
|
||
if (string.IsNullOrEmpty(Convert.ToString(旧值))) continue;
|
||
row[列s[i]] = DBNull.Value;
|
||
any = true;
|
||
continue;
|
||
}
|
||
if (tbl.Columns[列s[i]].DataType == typeof(double))
|
||
{
|
||
double dd;
|
||
double.TryParse(Convert.ToString(新值),
|
||
System.Globalization.NumberStyles.Float,
|
||
System.Globalization.CultureInfo.InvariantCulture, out dd);
|
||
新值 = dd;
|
||
}
|
||
if (Convert.ToString(新值) == Convert.ToString(旧值)) continue;
|
||
row[列s[i]] = 新值;
|
||
any = true;
|
||
}
|
||
}
|
||
if (any)
|
||
{
|
||
dgv.Invalidate();
|
||
Log("[显示同步] 元件表费用行已按库刷新(实时联动)");
|
||
}
|
||
}
|
||
catch (Exception exF) { Log("[显示同步] 费用行刷新异常: " + exF.Message); }
|
||
}
|
||
|
||
/// <summary>按内部ID对齐,从库重刷某表的价格列显示(通用)。
|
||
/// ★ 合并视图支持(2026-10-01):箱号合并表(有_来源表名_)按来源表逐表读库;
|
||
/// 图号合并表(有_来源图号_)统一读图号主表——删除/编辑后价格列照常刷新。</summary>
|
||
private void 刷新表价格列从库(DataGridView dgv, DataTable tbl, string 价列csv)
|
||
{
|
||
try
|
||
{
|
||
if (dgv == null || tbl == null) return;
|
||
// 合并视图判定(TableName 空但带来源列)
|
||
bool isMerged = string.IsNullOrEmpty(tbl.TableName) &&
|
||
(tbl.Columns.Contains("_来源表名_") || tbl.Columns.Contains("_来源图号_"));
|
||
if (string.IsNullOrEmpty(tbl.TableName) && !isMerged) return; // 无名表且非合并视图跳过
|
||
if (!tbl.Columns.Contains("内部ID")) return;
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
|
||
string dbPath = GetQuoteDbPath(_currentProjectId);
|
||
if (!System.IO.File.Exists(dbPath)) return;
|
||
|
||
// 读哪些物理表:箱号合并表按来源表逐表;图号合并表统一读图号主表;普通表读自身
|
||
var 读表s = new System.Collections.Generic.List<string>();
|
||
if (isMerged && tbl.Columns.Contains("_来源表名_"))
|
||
{
|
||
foreach (DataRow dr in tbl.Rows)
|
||
{
|
||
string src = Convert.ToString(dr["_来源表名_"]).Trim();
|
||
if (!string.IsNullOrWhiteSpace(src) && !读表s.Contains(src)) 读表s.Add(src);
|
||
}
|
||
}
|
||
else if (isMerged)
|
||
{
|
||
读表s.Add(TuhaoTableName);
|
||
}
|
||
else
|
||
{
|
||
读表s.Add(tbl.TableName);
|
||
}
|
||
|
||
var 库值 = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<string, object>>(
|
||
System.StringComparer.OrdinalIgnoreCase);
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
foreach (string srcTbl in 读表s)
|
||
{
|
||
try
|
||
{
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 内部ID, " + 价列csv + " FROM \"" + srcTbl.Replace("\"", "") + "\";", conn))
|
||
using (var r = cmd.ExecuteReader())
|
||
{
|
||
while (r.Read())
|
||
{
|
||
string iid = r.IsDBNull(0) ? "" : r.GetString(0).Trim();
|
||
if (string.IsNullOrEmpty(iid)) continue;
|
||
var d = new System.Collections.Generic.Dictionary<string, object>();
|
||
for (int i = 1; i < r.FieldCount; i++)
|
||
d[r.GetName(i)] = r.IsDBNull(i) ? (object)DBNull.Value : r.GetValue(i);
|
||
库值[iid] = d;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception exSrc) { Log("[显示同步] 读来源表 " + srcTbl + " 异常: " + exSrc.Message); }
|
||
}
|
||
}
|
||
string[] 价列 = 价列csv.Split(',');
|
||
bool any = false;
|
||
foreach (DataRow dr in tbl.Rows)
|
||
{
|
||
string iid = Convert.ToString(dr["内部ID"]).Trim();
|
||
System.Collections.Generic.Dictionary<string, object> d;
|
||
if (string.IsNullOrEmpty(iid) || !库值.TryGetValue(iid, out d)) continue;
|
||
foreach (string cRaw in 价列)
|
||
{
|
||
string c = cRaw.Trim();
|
||
if (!tbl.Columns.Contains(c) || !d.ContainsKey(c)) continue;
|
||
object v = d[c];
|
||
// 公式列保持数值类型(Format 依赖)
|
||
if (v != DBNull.Value && tbl.Columns[c].DataType == typeof(double))
|
||
{
|
||
double dd;
|
||
v = double.TryParse(Convert.ToString(v),
|
||
System.Globalization.NumberStyles.Float,
|
||
System.Globalization.CultureInfo.InvariantCulture, out dd) ? dd : (object)0d;
|
||
}
|
||
dr[c] = v;
|
||
any = true;
|
||
}
|
||
}
|
||
if (any)
|
||
{
|
||
dgv.Invalidate();
|
||
Log("[显示同步] " + (string.IsNullOrEmpty(tbl.TableName) ? "(合并视图)" : tbl.TableName) +
|
||
" 价格列已按库刷新(内部ID对齐)");
|
||
}
|
||
}
|
||
catch (Exception exF) { Log("[显示同步] 刷新 " + (tbl != null ? tbl.TableName : "?") + " 异常: " + exF.Message); }
|
||
}
|
||
|
||
/// <summary>图号/箱号重名自动后缀:值与同表其他行(除自身行)重复时返回 "名(2)"/"名(3)"…;
|
||
/// 不重复返回原值。空值不处理。</summary>
|
||
private static object 重名自动后缀(System.Data.DataTable table, int 自身行, string colName, object 新值)
|
||
{
|
||
if (新值 == null || 新值 == DBNull.Value) return 新值;
|
||
string v = 新值.ToString().Trim();
|
||
if (v.Length == 0) return 新值;
|
||
bool 撞 = false;
|
||
foreach (System.Data.DataRow r in table.Rows)
|
||
{
|
||
if (r.RowState == System.Data.DataRowState.Deleted) continue;
|
||
if (table.Rows.IndexOf(r) == 自身行) continue;
|
||
if (string.Equals((r[colName] == null || r[colName] == DBNull.Value) ? "" : r[colName].ToString().Trim(), v, System.StringComparison.Ordinal))
|
||
{ 撞 = true; break; }
|
||
}
|
||
if (!撞) return 新值;
|
||
for (int n = 2; n < 1000; n++)
|
||
{
|
||
string 候 = v + "(" + n + ")";
|
||
bool 再撞 = false;
|
||
foreach (System.Data.DataRow r in table.Rows)
|
||
{
|
||
if (r.RowState == System.Data.DataRowState.Deleted) continue;
|
||
if (string.Equals((r[colName] == null || r[colName] == DBNull.Value) ? "" : r[colName].ToString().Trim(), 候, System.StringComparison.Ordinal))
|
||
{ 再撞 = true; break; }
|
||
}
|
||
if (!再撞) return 候;
|
||
}
|
||
return 新值; // 找不到空位(几乎不可能)就用原值
|
||
}
|
||
|
||
// ============================================================
|
||
// 价格显示格式化(用 DefaultCellStyle.Format,只改显示不改值)
|
||
// - 这是 DataGridView 原生的“只改显示”机制,不干扰用户输入
|
||
// - 仅作用于公式计算列(见 s_公式列集合);用户输入列(表价/数量/系数)不参与
|
||
// ============================================================
|
||
|
||
/// <summary>公式计算列集合(只包含程序算出来的价格列,不包含用户输入的表价)。
|
||
/// ★ 口径(2026-09-29):毛利率(百分数值 4.76)入集合获得小数位格式化;
|
||
/// 利润率(小数 0.05)不入集合,保持原样显示。</summary>
|
||
private static readonly System.Collections.Generic.HashSet<string> s_公式列集合 =
|
||
new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
// 元件层(表价是用户输入,也需要格式化显示)
|
||
"表价", "采购单价", "采购价", "报出单价", "报出价",
|
||
// 箱号层公式列
|
||
"单台成本", "成本价", "单台报出", "利润", "毛利率",
|
||
// 图号层公式列
|
||
"单价", "总价", "成本总价", "毛利",
|
||
// 项目层公式列
|
||
"成本总价", "报出总价", "利润"
|
||
};
|
||
|
||
/// <summary>
|
||
/// 把小数位配置(“保留整数”/“保留2位小数”等)转换成 .NET 格式字符串。
|
||
/// 例:“保留整数” → "F0";“保留2位小数” → "F2”。
|
||
/// 用 F 而不是 N(不要千分位逗号)。
|
||
/// </summary>
|
||
private static string ToFormatString(string 小数位文本)
|
||
{
|
||
int n = 价格格式化.解析小数位(小数位文本);
|
||
if (n < 0) n = 2; // 未配置默认 2 位
|
||
return "F" + n.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 老库兼容:把 DataTable 中公式列的类型从 string 转为 double。
|
||
/// 新库建表时公式列是 REAL(Fill 后自动是 double);老库可能是 TEXT(string),
|
||
/// string 值的 DefaultCellStyle.Format 不生效,需要转换。
|
||
/// 如果公式列已经是 double 则原样返回。
|
||
/// </summary>
|
||
private static DataTable ConvertFormulaColumnsToDouble(DataTable table)
|
||
{
|
||
if (table == null || table.Columns.Count == 0) return table;
|
||
|
||
// 检查是否有公式列是 string 类型(老库)
|
||
bool needsConvert = false;
|
||
foreach (DataColumn dc in table.Columns)
|
||
{
|
||
if (s_公式列集合.Contains(dc.ColumnName) && dc.DataType == typeof(string))
|
||
{
|
||
needsConvert = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!needsConvert) return table;
|
||
|
||
// 新建 DataTable,公式列用 double,其他列保持原类型
|
||
var newTable = new DataTable();
|
||
foreach (DataColumn dc in table.Columns)
|
||
{
|
||
Type t = s_公式列集合.Contains(dc.ColumnName) ? typeof(double) : dc.DataType;
|
||
newTable.Columns.Add(dc.ColumnName, t);
|
||
}
|
||
|
||
// 复制数据,公式列从 string 解析为 double
|
||
foreach (DataRow row in table.Rows)
|
||
{
|
||
var newRow = newTable.NewRow();
|
||
foreach (DataColumn dc in table.Columns)
|
||
{
|
||
if (s_公式列集合.Contains(dc.ColumnName))
|
||
{
|
||
object v = row[dc];
|
||
if (v == null || v == DBNull.Value)
|
||
newRow[dc.ColumnName] = 0d;
|
||
else if (v is double)
|
||
newRow[dc.ColumnName] = v;
|
||
else
|
||
{
|
||
double d;
|
||
newRow[dc.ColumnName] = double.TryParse(v.ToString().Trim(), out d) ? d : 0d;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
newRow[dc.ColumnName] = row[dc];
|
||
}
|
||
}
|
||
newTable.Rows.Add(newRow);
|
||
}
|
||
newTable.TableName = table.TableName;
|
||
return newTable;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 给指定 DataGridView 的所有公式计算列应用小数位格式。
|
||
/// 只设 DefaultCellStyle.Format,不设 ValueType(避免 DataError 导致单元格不显示)。
|
||
/// ★ 同一表格+同一格式只应用一次(每次 Reload 重跑是重复开销);小数位配置变化时由
|
||
/// ApplyPriceFormatsToAllGrids 清缓存强制重刷。
|
||
/// </summary>
|
||
private static readonly System.Collections.Generic.HashSet<string> _已应用格式 =
|
||
new System.Collections.Generic.HashSet<string>();
|
||
|
||
private void ApplyPriceFormat(DataGridView dgv, string 小数位文本)
|
||
{
|
||
if (dgv == null || dgv.Columns == null) return;
|
||
string fmt = ToFormatString(小数位文本);
|
||
string key = dgv.Name + "|" + fmt;
|
||
lock (_已应用格式)
|
||
{
|
||
if (!_已应用格式.Add(key)) return; // 已应用过,跳过
|
||
}
|
||
|
||
// 1) 设 Format + ValueType
|
||
foreach (DataGridViewColumn col in dgv.Columns)
|
||
{
|
||
if (col == null) continue;
|
||
string name = !string.IsNullOrEmpty(col.DataPropertyName)
|
||
? col.DataPropertyName : col.HeaderText;
|
||
if (string.IsNullOrEmpty(name)) continue;
|
||
if (s_公式列集合.Contains(name))
|
||
{
|
||
// ★ 毛利率带 % 显示(2026-09-29):值已是百分数值(4.76),
|
||
// 用字面量 % 格式(不能用 P 格式,那会再乘100)
|
||
if (string.Equals(name, "毛利率", System.StringComparison.OrdinalIgnoreCase))
|
||
col.DefaultCellStyle.Format = "0.##\"%\"";
|
||
else
|
||
col.DefaultCellStyle.Format = fmt;
|
||
col.DefaultCellStyle.NullValue = "";
|
||
col.ValueType = typeof(double);
|
||
}
|
||
}
|
||
|
||
// 2) 非虚拟模式的表格(如项目表):保留原"遍历行把公式列 string 转 double"的兼容逻辑。
|
||
// 虚拟模式的三表(9/4/5)跳过——写 cell.Value 无效,数值化在读通道 Dgv_CellValueNeeded 处理。
|
||
if (dgv.VirtualMode) return;
|
||
|
||
if (dgv.Rows != null)
|
||
{
|
||
foreach (DataGridViewRow row in dgv.Rows)
|
||
{
|
||
if (row == null || row.IsNewRow) continue;
|
||
foreach (DataGridViewColumn col in dgv.Columns)
|
||
{
|
||
if (col == null) continue;
|
||
string name = !string.IsNullOrEmpty(col.DataPropertyName)
|
||
? col.DataPropertyName : col.HeaderText;
|
||
if (!s_公式列集合.Contains(name)) continue;
|
||
var cell = row.Cells[col.Index];
|
||
if (cell == null || cell.Value == null) continue;
|
||
if (cell.Value is double) continue;
|
||
if (cell.Value is decimal)
|
||
{
|
||
cell.Value = (double)(decimal)cell.Value;
|
||
continue;
|
||
}
|
||
string s = cell.Value.ToString().Trim();
|
||
double d;
|
||
if (double.TryParse(s, out d))
|
||
{
|
||
cell.Value = d;
|
||
}
|
||
else if (string.IsNullOrEmpty(s))
|
||
{
|
||
cell.Value = 0d;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 母排元件行不受项目"小数位/取整方式"设置限制:价格列按自然值显示
|
||
/// (0.########## = 有几位小数显几位、不四舍五入、无尾零),其余行仍走列级设置格式。
|
||
/// 仅元件表挂接;虚拟模式行号与内存表 1:1(同 Dgv_CellValueNeeded 的取数通道),
|
||
/// 经"类型"列=="母排"判定(母排工具写入的行见 主窗口.母排与备份.cs)。
|
||
/// </summary>
|
||
private void Grid5_母排CellFormatting(object sender, System.Windows.Forms.DataGridViewCellFormattingEventArgs e)
|
||
{
|
||
try
|
||
{
|
||
var dgv = sender as System.Windows.Forms.DataGridView;
|
||
if (dgv == null || e.ColumnIndex < 0 || e.RowIndex < 0) return;
|
||
var col = dgv.Columns[e.ColumnIndex];
|
||
if (col == null) return;
|
||
string colName = col.DataPropertyName;
|
||
if (string.IsNullOrEmpty(colName) || !s_公式列集合.Contains(colName)) return;
|
||
var table = 取dgv数据表(dgv);
|
||
if (table == null || e.RowIndex >= table.Rows.Count || !table.Columns.Contains("类型")) return;
|
||
object t = table.Rows[e.RowIndex]["类型"];
|
||
if (t != null && t != DBNull.Value && t.ToString().Trim() == "母排")
|
||
e.CellStyle.Format = "0.##########";
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 进入项目后,按当前项目的价格格式化配置,应用 DefaultCellStyle.Format 到 4 个表格的公式列。
|
||
/// 同时把 DataTable 里公式列的值从 string 转成 decimal(Format 只对数值类型生效)。
|
||
/// </summary>
|
||
private void ApplyPriceFormatsToAllGrids()
|
||
{
|
||
lock (_已应用格式) { _已应用格式.Clear(); } // 小数位配置变化,强制全部重刷
|
||
// 单台层:元件表/箱号表/图号表
|
||
ApplyPriceFormat(this.advancedDataGridView5, _单台小数位文本);
|
||
ApplyPriceFormat(this.advancedDataGridView4, _单台小数位文本);
|
||
ApplyPriceFormat(this.advancedDataGridView9, _单台小数位文本);
|
||
// 项目层:项目表
|
||
ApplyPriceFormat(this.advancedDataGridView6, _项目小数位文本);
|
||
}
|
||
|
||
/// <summary>判断当前表格是不是项目表(advancedDataGridView6)。项目表用项目层配置。</summary>
|
||
private static bool IsProjectGrid(DataGridView dgv)
|
||
{
|
||
// advancedDataGridView6 是项目表;其他三个(9/4/5)是单台层
|
||
// 用名称匹配,避免依赖字段顺序
|
||
return dgv != null && dgv.Name != null && dgv.Name.IndexOf("6", System.StringComparison.Ordinal) >= 0
|
||
&& dgv.Name.IndexOf("advancedDataGridView", System.StringComparison.OrdinalIgnoreCase) >= 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从报价库的「项目信息」表读取当前项目的 4 个价格格式化配置字段,
|
||
/// 更新到内存配置变量。库/表/字段不存在时保持默认值(保留2位小数/不取整)。
|
||
/// </summary>
|
||
private void LoadPriceFormatConfig(string projectId)
|
||
{
|
||
// 先重置为默认值,避免上一个项目的配置串过来
|
||
_单台小数位文本 = "保留2位小数";
|
||
_单台取整方式 = "不取整";
|
||
_项目小数位文本 = "保留2位小数";
|
||
_项目取整方式 = "不取整";
|
||
|
||
try
|
||
{
|
||
var info = GetProjectInfoFromQuoteDb(projectId);
|
||
if (info == null || info.Count == 0) return;
|
||
|
||
string v;
|
||
if (info.TryGetValue("单台小数位", out v) && !string.IsNullOrWhiteSpace(v)) _单台小数位文本 = v;
|
||
if (info.TryGetValue("单台取整方式", out v) && !string.IsNullOrWhiteSpace(v)) _单台取整方式 = v;
|
||
if (info.TryGetValue("项目小数位", out v) && !string.IsNullOrWhiteSpace(v)) _项目小数位文本 = v;
|
||
if (info.TryGetValue("项目取整方式", out v) && !string.IsNullOrWhiteSpace(v)) _项目取整方式 = v;
|
||
}
|
||
catch
|
||
{
|
||
// 读不到不阻断,沿用默认值
|
||
}
|
||
|
||
// 配置加载完毕,统一应用到 4 个表格的公式列(DefaultCellStyle.Format)
|
||
ApplyPriceFormatsToAllGrids();
|
||
}
|
||
|
||
/// <summary>按 DataPropertyName 或 HeaderText 找列(因为 UI 隐藏了部分列)。</summary>
|
||
private static DataGridViewColumn FindColumn(DataGridView dgv, string name)
|
||
{
|
||
if (dgv == null || dgv.Columns == null) return null;
|
||
foreach (DataGridViewColumn c in dgv.Columns)
|
||
{
|
||
if (c == null) continue;
|
||
if (c.DataPropertyName == name || c.HeaderText == name || c.Name == name) return c;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>取某行某列的字符串值(按 DataPropertyName/HeaderText/Name 匹配)。</summary>
|
||
private static string GetCellString(DataGridViewRow row, string name)
|
||
{
|
||
if (row == null || row.DataGridView == null) return "";
|
||
DataGridViewColumn col = FindColumn(row.DataGridView, name);
|
||
if (col == null) return "";
|
||
object v = row.Cells[col.Index].Value;
|
||
return v == null ? "" : v.ToString().Trim();
|
||
}
|
||
|
||
/// <summary>取某行某列的 decimal 值。</summary>
|
||
private static decimal GetCellDecimal(DataGridViewRow row, string name)
|
||
{
|
||
return ParseDecimal(GetCellString(row, name));
|
||
}
|
||
|
||
// ---------- 元件表重算(每行独立) ----------
|
||
|
||
/// <summary>
|
||
/// 重算单行元件的派生列(对应 advancedDataGridView5Table Excel 公式):
|
||
/// 采购单价(I) = 表价(H) × 采购系数(J)
|
||
/// 采购价(K) = 采购单价(I) × 数量(G)
|
||
/// 报出单价(L) = 采购单价(I) × 报出系数(M)
|
||
/// 报出价(N) = 报出单价(L) × 数量(G)
|
||
/// </summary>
|
||
private void RecalcYuanjianRow(DataGridViewRow row)
|
||
{
|
||
if (row == null) return;
|
||
|
||
// 按 advancedDataGridView5Table Excel 公式:
|
||
// 采购单价(I) = 表价(H) × 采购系数(J)
|
||
// 采购价(K) = 采购单价(I) × 数量(G)
|
||
// 报出单价(L) = 采购单价(I) × 报出系数(M)
|
||
// 报出价(N) = 报出单价(L) × 数量(G)
|
||
decimal biaoJia = GetCellDecimal(row, "表价");
|
||
decimal caiGouXiShu = GetCellDecimal(row, "采购系数");
|
||
decimal baoChuXiShu = GetCellDecimal(row, "报出系数");
|
||
decimal shuLiang = GetCellDecimal(row, "数量");
|
||
|
||
// ★ A2(2026-09-30):公式统一调 报价公式(单一来源,三处重算同源)
|
||
decimal caiGouDanJia, caiGouJia, baoChuDanJia, baoChuJia;
|
||
报价公式.元件(biaoJia, caiGouXiShu, baoChuXiShu, shuLiang,
|
||
out caiGouDanJia, out caiGouJia, out baoChuDanJia, out baoChuJia);
|
||
|
||
SetCell(row, "采购单价", caiGouDanJia);
|
||
SetCell(row, "采购价", caiGouJia);
|
||
SetCell(row, "报出单价", baoChuDanJia);
|
||
SetCell(row, "报出价", baoChuJia);
|
||
}
|
||
|
||
// ---------- 箱号表重算(聚合当前箱号所有元件) ----------
|
||
|
||
/// <summary>
|
||
/// 聚合当前箱号(_currentTuhao + _currentXianghao)的所有元件,
|
||
/// 重算箱号表当前行的派生列(对应 Excel 箱号表公式,合计行引用):
|
||
/// 单台成本 = SUM(该箱号元件.采购价)
|
||
/// 成本价 = 单台成本 × 数量
|
||
/// 单台报出 = SUM(该箱号元件.报出价)
|
||
/// 报出价 = 单台报出 × 数量
|
||
/// 利润 = 报出价 − 成本价
|
||
/// 利润率 = 利润 / 成本价 (小数,0.05=5%)
|
||
/// 毛利率 = 利润 / 报出价 × 100 (百分数值,4.76=4.76%)
|
||
/// </summary>
|
||
private void RecalcXianghaoRowFromYuanjian(DataGridViewRow xianghaoRow)
|
||
{
|
||
if (xianghaoRow == null) return;
|
||
|
||
// 聚合当前箱号下所有元件的 采购价/报出价
|
||
decimal sumCaiGouJia = 0m;
|
||
decimal sumBaoChuJia = 0m;
|
||
DataGridView dgv5 = this.advancedDataGridView5;
|
||
if (dgv5 != null && dgv5.Rows != null)
|
||
{
|
||
foreach (DataGridViewRow yj in dgv5.Rows)
|
||
{
|
||
if (yj == null || yj.IsNewRow) continue;
|
||
sumCaiGouJia += GetCellDecimal(yj, "采购价");
|
||
sumBaoChuJia += GetCellDecimal(yj, "报出价");
|
||
}
|
||
}
|
||
|
||
decimal shuLiang = GetCellDecimal(xianghaoRow, "数量");
|
||
// ★ A2(2026-09-30):公式统一调 报价公式(口径:利润率小数,毛利率百分数值)
|
||
decimal danTaiChengBen, chengBenJia, danTaiBaoChu, baoChuJia, liRun, liRunLv, maoLiLv;
|
||
报价公式.箱号(sumCaiGouJia, sumBaoChuJia, shuLiang,
|
||
out danTaiChengBen, out chengBenJia, out danTaiBaoChu, out baoChuJia,
|
||
out liRun, out liRunLv, out maoLiLv);
|
||
|
||
SetCell(xianghaoRow, "单台成本", danTaiChengBen);
|
||
SetCell(xianghaoRow, "成本价", chengBenJia);
|
||
SetCell(xianghaoRow, "单台报出", danTaiBaoChu);
|
||
SetCell(xianghaoRow, "报出价", baoChuJia);
|
||
SetCell(xianghaoRow, "利润", liRun);
|
||
SetCell(xianghaoRow, "利润率", liRunLv);
|
||
SetCell(xianghaoRow, "毛利率", maoLiLv);
|
||
}
|
||
|
||
// ---------- 图号表重算(聚合当前图号所有箱号) ----------
|
||
|
||
/// <summary>
|
||
/// 聚合当前图号(_currentTuhao)的所有箱号,
|
||
/// 重算图号表当前行的派生列(对应 Excel 图号表公式,合计行引用):
|
||
/// 箱柜数量 = SUM(该图号箱号.数量)
|
||
/// 单价 = SUM(该图号箱号.报出价)
|
||
/// 总价 = 单价 × 台数
|
||
/// 成本总价 = SUM(该图号箱号.成本价) × 台数
|
||
/// 毛利 = 总价 − 成本总价
|
||
/// 毛利率 = 毛利 / 总价 × 100 (百分数值,与项目页/方案页口径一致)
|
||
/// </summary>
|
||
private void RecalcTuhaoRowFromXianghao(DataGridViewRow tuhaoRow)
|
||
{
|
||
if (tuhaoRow == null) return;
|
||
|
||
// 从数据库重新读取该图号所有箱号的聚合(因为其他箱号可能不在当前表格视图里)
|
||
// 当前 advancedDataGridView4 只显示当前图号的箱号,所以可以直接聚合表格行
|
||
decimal sumShuLiang = 0m;
|
||
decimal sumBaoChuJia = 0m;
|
||
decimal sumChengBenJia = 0m;
|
||
DataGridView dgv4 = this.advancedDataGridView4;
|
||
if (dgv4 != null && dgv4.Rows != null)
|
||
{
|
||
foreach (DataGridViewRow xh in dgv4.Rows)
|
||
{
|
||
if (xh == null || xh.IsNewRow) continue;
|
||
sumShuLiang += GetCellDecimal(xh, "数量");
|
||
sumBaoChuJia += GetCellDecimal(xh, "报出价");
|
||
sumChengBenJia += GetCellDecimal(xh, "成本价");
|
||
}
|
||
}
|
||
|
||
decimal taiShu = GetCellDecimal(tuhaoRow, "台数");
|
||
// ★ A2(2026-09-30):公式统一调 报价公式(总价=单价×台数,不乘箱柜数量——单价已是全部箱号报出价合计)
|
||
decimal xiangGuiShuLiang, danJia, zongJia, chengBenZongJia, maoLi, maoLiLv;
|
||
报价公式.图号(sumShuLiang, sumBaoChuJia, sumChengBenJia, taiShu,
|
||
out xiangGuiShuLiang, out danJia, out zongJia, out chengBenZongJia,
|
||
out maoLi, out maoLiLv);
|
||
|
||
// ★ 排障:图号重算读值对照(grid 读值 vs 内存表值),定位"报出价读到成本值"的分裂点
|
||
try
|
||
{
|
||
var xhTable = 取dgv数据表(dgv4);
|
||
string trace = "";
|
||
foreach (DataGridViewRow xh in dgv4.Rows)
|
||
{
|
||
if (xh == null || xh.IsNewRow) continue;
|
||
string g报价 = GetCellString(xh, "报出价"), g成本 = GetCellString(xh, "成本价");
|
||
string m报价 = (xhTable != null && xh.Index < xhTable.Rows.Count && xhTable.Columns.Contains("报出价"))
|
||
? Convert.ToString(xhTable.Rows[xh.Index]["报出价"]) : "(无表)";
|
||
trace += "[行" + xh.Index + " grid报出=" + g报价 + " 表报出=" + m报价 + " grid成本=" + g成本 + "] ";
|
||
}
|
||
Log("[图号重算trace] 台数=" + taiShu + " sum报出=" + sumBaoChuJia + " sum成本=" + sumChengBenJia + " " + trace);
|
||
}
|
||
catch { }
|
||
|
||
SetCell(tuhaoRow, "箱柜数量", xiangGuiShuLiang);
|
||
SetCell(tuhaoRow, "单价", danJia);
|
||
SetCell(tuhaoRow, "总价", zongJia);
|
||
SetCell(tuhaoRow, "成本总价", chengBenZongJia);
|
||
SetCell(tuhaoRow, "毛利", maoLi);
|
||
SetCell(tuhaoRow, "毛利率", maoLiLv);
|
||
|
||
// 图号重算完,联动重算项目表(第 4 级聚合)并立即存库
|
||
RecalcProjectRowFromTuhao();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 数据加载(Reload)后的重算:让新建/删除后受影响的行立即显示计算结果。
|
||
/// 注意:箱号行/图号行的重算依赖“该箱号/图号自己名下的子数据”,
|
||
/// 而 UI 表格一次只显示当前选中层的数据,所以这里只能重算:
|
||
/// - 所有元件行(底层,每行自洽)
|
||
/// - 当前选中的箱号行(聚合当前元件表)
|
||
/// - 当前选中的图号行(聚合当前箱号表)
|
||
/// 不能遍历箱号/图号表所有行去重算(否则会把所有行算成当前层的数据,串数据)。
|
||
/// 重算后存库 + AcceptChanges,避免下次 Reload 重复写入。
|
||
/// </summary>
|
||
private void RecalcAllAfterReload()
|
||
{
|
||
// 没有项目时无需重算
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
|
||
|
||
// 双 guard:既挡 CellValueChanged/Grid_DirtyStateChanged,也挡 SelectionChanged,
|
||
// 防止 Save 里的 EndEdit 触发联动刷新形成死循环。
|
||
_isRecalculating = true;
|
||
_isReloading = true;
|
||
try
|
||
{
|
||
// 1) 重算所有元件行(每行独立,数据自洽)
|
||
DataGridView dgv5 = this.advancedDataGridView5;
|
||
if (dgv5 != null && dgv5.Rows != null)
|
||
{
|
||
foreach (DataGridViewRow yj in dgv5.Rows)
|
||
{
|
||
if (yj == null || yj.IsNewRow) continue;
|
||
RecalcYuanjianRow(yj);
|
||
}
|
||
}
|
||
|
||
// 2) 只重算当前选中的箱号行(它聚合的是当前元件表的数据)
|
||
var xhRow = GetFirstSelectedRow(this.advancedDataGridView4);
|
||
if (xhRow != null)
|
||
{
|
||
RecalcXianghaoRowFromYuanjian(xhRow);
|
||
}
|
||
|
||
// 3) 只重算当前选中的图号行(它聚合的是当前箱号表的数据)
|
||
var thRow = GetFirstSelectedRow(this.advancedDataGridView9);
|
||
if (thRow != null)
|
||
{
|
||
RecalcTuhaoRowFromXianghao(thRow);
|
||
}
|
||
|
||
// 重算改了单元格值,现在把完整内存数据存库,然后 AcceptChanges。
|
||
// 顺序很重要:先 Save(删旧行+插当前所有行),再 AcceptChanges(让内存标记为干净)。
|
||
// 这样既持久化了重算结果,又避免下次 Reload 开头的 Save 重复写入(串数据 bug)。
|
||
// ★ 触发器已退役(改为应用层集中刷新),派生值不会自动落库,必须显式 Save。
|
||
// 否则粘贴/插入后 RecalcAllAfterReload 重算的 采购单价/采购价/报出单价/报出价 等
|
||
// 只在内存,刷新(Reload)后从库读到的是 INSERT 时的原始值,重算结果丢失。
|
||
try
|
||
{
|
||
if (_yuanjianTable != null && _yuanjianTable.Rows.Count > 0 && !string.IsNullOrWhiteSpace(_yuanjianTable.TableName))
|
||
SaveTableToDatabase(_yuanjianTable, _currentProjectId, "\"" + _yuanjianTable.TableName + "\"", null, null, true);
|
||
if (_xianghaoTable != null && _xianghaoTable.Rows.Count > 0 && !string.IsNullOrWhiteSpace(_xianghaoTable.TableName))
|
||
SaveTableToDatabase(_xianghaoTable, _currentProjectId, "\"" + _xianghaoTable.TableName + "\"", null, null, true);
|
||
if (_tuhaoTable != null && _tuhaoTable.Rows.Count > 0)
|
||
SaveTableToDatabase(_tuhaoTable, _currentProjectId, TuhaoTableName, null, null, true);
|
||
}
|
||
catch (Exception exSave) { Log("[RecalcAllAfterReload] 保存重算结果失败: " + exSave.Message); }
|
||
if (_yuanjianTable != null) _yuanjianTable.AcceptChanges();
|
||
if (_xianghaoTable != null) _xianghaoTable.AcceptChanges();
|
||
if (_tuhaoTable != null) _tuhaoTable.AcceptChanges();
|
||
}
|
||
finally
|
||
{
|
||
_isRecalculating = false;
|
||
_lastRecalcTime = DateTime.Now; // 记录重算结束时间,Grid_DirtyStateChanged 用此过滤假变脏
|
||
_isReloading = false;
|
||
刷新状态栏(); // 状态栏:重算时间更新
|
||
}
|
||
|
||
// ★ 项目页全局同步收口:几乎所有数据变更(新建图号/箱号/元件、复制粘贴、删除、
|
||
// 各工具窗口插入、编辑)最终都经过本方法——在此触发一次,同步就全局生效
|
||
// (800ms 防抖自动合并频发调用),不再只靠单元格编辑那一个入口。
|
||
启动项目页同步();
|
||
}
|
||
|
||
// ---------- 全量数据库重算(不依赖内存/选中状态) ----------
|
||
|
||
/// <summary>
|
||
/// 直接读数据库,把指定项目下所有图号、所有箱号的派生列重算并 UPDATE 回数据库。
|
||
/// 解决"只重算当前选中行,没点过的箱号/图号派生列永远是空"的问题。
|
||
/// 重算公式与 Excel 模板、RecalcXianghaoRowFromYuanjian、RecalcTuhaoRowFromXianghao 完全一致:
|
||
/// 箱号行:单台成本=SUM(元件.采购价), 成本价=单台成本×数量, 单台报出=SUM(元件.报出价),
|
||
/// 报出价=单台报出×数量, 利润=报出价−成本价, 利润率=(成本价=0?0:利润/成本价), 毛利率=(报出价=0?0:利润/报出价×100)
|
||
/// 图号行:箱柜数量=SUM(箱号.数量), 单价=SUM(箱号.报出价), 总价=单价×箱柜数量×台数,
|
||
/// 成本总价=SUM(箱号.成本价)×台数, 毛利=总价−成本总价, 毛利率=(总价=0?0:毛利/总价×100)
|
||
/// 项目行(写回项目库索引):箱柜台数=SUM(图号.箱柜数量), 成本总价=SUM(图号.成本总价),
|
||
/// 报出总价=SUM(图号.总价), 利润=报出总价−成本总价, 利润率=(成本总价=0?0:利润/成本总价)
|
||
/// ★ 口径(2026-09-29):箱号/图号的毛利率存百分数值(×100,保留2位,如 4.76=4.76%);
|
||
/// 利润率统一存小数(保留2位,0.05=5%)
|
||
/// </summary>
|
||
/// <param name="projectId">项目编号;空则用 _currentProjectId。</param>
|
||
/// <param name="scope">★ B2(2026-10-01)增量重算范围:键="图号\n箱号内部ID"。
|
||
/// null/空=全量重算(进项目/删改等低频操作);
|
||
/// 非空=只重算范围内的箱号行(从元件逐行重算),其图号行聚合用"重算新值+未受影响箱号库值",
|
||
/// 项目汇总用"全部图号聚合列(受影响图号新值覆盖)",大项目编辑不再全库重算。</param>
|
||
private void RecalcAllFromDatabase(string projectId,
|
||
System.Collections.Generic.HashSet<string> scope = null)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(projectId)) projectId = _currentProjectId;
|
||
if (string.IsNullOrWhiteSpace(projectId)) return;
|
||
string dbPath = GetQuoteDbPath(projectId);
|
||
if (!System.IO.File.Exists(dbPath)) return;
|
||
|
||
// ★ B3(2026-10-01):重算耗时观测(全量/增量分别统计,日志可对比)
|
||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||
bool incremental = scope != null && scope.Count > 0;
|
||
|
||
try
|
||
{
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
|
||
// ★ B2(2026-10-01):全部派生列/聚合 UPDATE 聚成单事务提交——
|
||
// 原先每条 UPDATE 自动提交(一次提交=一次锁竞争+日志),箱号多时放大耗时;
|
||
// DEFERRED 事务首个写操作才上写锁,读阶段不阻塞直写队列;异常自动回滚(全有或全无)。
|
||
var recalcTx = conn.BeginTransaction();
|
||
|
||
// ★ B2 增量分支:只重算受影响箱号,图号聚合混合新值+库值,项目汇总读全部图号聚合列
|
||
// scope 按图号分组: 图号 → 箱号内部ID集合
|
||
var thGroups = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.HashSet<string>>();
|
||
if (incremental)
|
||
{
|
||
foreach (var k in scope)
|
||
{
|
||
if (string.IsNullOrEmpty(k)) continue;
|
||
int sp = k.IndexOf('\n');
|
||
if (sp <= 0 || sp >= k.Length - 1) continue;
|
||
string th = k.Substring(0, sp);
|
||
string xh = k.Substring(sp + 1);
|
||
System.Collections.Generic.HashSet<string> set;
|
||
if (!thGroups.TryGetValue(th, out set))
|
||
{
|
||
set = new System.Collections.Generic.HashSet<string>();
|
||
thGroups[th] = set;
|
||
}
|
||
set.Add(xh);
|
||
}
|
||
if (thGroups.Count == 0) incremental = false; // scope 键全部无效→回退全量
|
||
}
|
||
|
||
if (incremental)
|
||
{
|
||
// 全部图号行的聚合值(内部ID→[箱柜数量,成本总价,总价];受影响图号稍后用新值覆盖)
|
||
var thAgg = new System.Collections.Generic.Dictionary<string, decimal[]>();
|
||
using (var selAgg = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT 内部ID, 箱柜数量, 成本总价, 总价 FROM " + TuhaoTableName + ";", conn))
|
||
using (var rdAgg = selAgg.ExecuteReader())
|
||
{
|
||
while (rdAgg.Read())
|
||
{
|
||
string iid = rdAgg.IsDBNull(0) ? "" : rdAgg.GetValue(0).ToString().Trim();
|
||
if (string.IsNullOrEmpty(iid)) continue;
|
||
thAgg[iid] = new[]
|
||
{
|
||
ParseDecimal(rdAgg.IsDBNull(1) ? "" : rdAgg.GetValue(1)),
|
||
ParseDecimal(rdAgg.IsDBNull(2) ? "" : rdAgg.GetValue(2)),
|
||
ParseDecimal(rdAgg.IsDBNull(3) ? "" : rdAgg.GetValue(3))
|
||
};
|
||
}
|
||
}
|
||
|
||
foreach (var g in thGroups)
|
||
{
|
||
string th = g.Key;
|
||
try
|
||
{
|
||
// 图号行:rowid+台数(按内部ID)
|
||
long thRid = 0;
|
||
decimal taiShu = 0m;
|
||
using (var selTh = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT rowid, 台数 FROM " + TuhaoTableName + " WHERE 内部ID=@th;", conn))
|
||
{
|
||
selTh.Parameters.AddWithValue("@th", th);
|
||
using (var rdTh = selTh.ExecuteReader())
|
||
{
|
||
if (rdTh.Read())
|
||
{
|
||
thRid = rdTh.GetInt64(0);
|
||
taiShu = ParseDecimal(rdTh.IsDBNull(1) ? "" : rdTh.GetValue(1));
|
||
}
|
||
}
|
||
}
|
||
if (thRid <= 0) continue; // 图号已删,跳过
|
||
|
||
// 该图号全部箱号行:rowid/数量/内部ID + 聚合列(报出价/成本价——未受影响箱号直接用库值)
|
||
string xhTable = GetXianghaoTableByIndex(th, conn);
|
||
if (string.IsNullOrEmpty(xhTable))
|
||
{
|
||
// 无箱号表:图号派生列置0
|
||
UpdateTuhaoCalc(conn, thRid, 0m, 0m, 0m, 0m, 0m, 0m);
|
||
thAgg[th] = new decimal[] { 0m, 0m, 0m };
|
||
continue;
|
||
}
|
||
string xhQuoted = "\"" + xhTable + "\"";
|
||
decimal sumShu = 0m, sumBC = 0m, sumCB = 0m;
|
||
var xhList = new List<XianghaoCalcRow>();
|
||
using (var selXh = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT rowid, 数量, 内部ID, 报出价, 成本价 FROM " + xhQuoted + " ORDER BY rowid;", conn))
|
||
using (var rdXh = selXh.ExecuteReader())
|
||
{
|
||
while (rdXh.Read())
|
||
{
|
||
var x = new XianghaoCalcRow();
|
||
x.RowId = rdXh.GetInt64(0);
|
||
x.ShuLiang = ParseDecimal(rdXh.IsDBNull(1) ? "" : rdXh.GetValue(1));
|
||
x.Xianghao = rdXh.IsDBNull(2) ? "" : rdXh.GetValue(2).ToString();
|
||
x.BaoChuJia = ParseDecimal(rdXh.IsDBNull(3) ? "" : rdXh.GetValue(3));
|
||
x.ChengBenJia = ParseDecimal(rdXh.IsDBNull(4) ? "" : rdXh.GetValue(4));
|
||
xhList.Add(x);
|
||
}
|
||
}
|
||
|
||
foreach (var x in xhList)
|
||
{
|
||
if (g.Value.Contains(x.Xianghao))
|
||
{
|
||
// 受影响箱号:从元件逐行重算
|
||
decimal sumCaiGou = 0m, sumBaoChu = 0m;
|
||
string yjTable = GetYuanjianTableByIndex(th, x.Xianghao, conn);
|
||
if (!string.IsNullOrEmpty(yjTable))
|
||
{
|
||
string yjQuoted = "\"" + yjTable + "\"";
|
||
// ★ B2(2026-10-01):与全量同款——含费用行实时联动
|
||
重算元件表聚合含费用联动(conn, yjQuoted, out sumCaiGou, out sumBaoChu);
|
||
}
|
||
// ★ A2(2026-09-30):公式统一调 报价公式(利润率小数/毛利率百分数值)
|
||
decimal danTaiChengBen, chengBenJia, danTaiBaoChu, baoChuJia, xhLiRun, xhLiRunLv, xhMaoLiLv;
|
||
报价公式.箱号(sumCaiGou, sumBaoChu, x.ShuLiang,
|
||
out danTaiChengBen, out chengBenJia, out danTaiBaoChu, out baoChuJia,
|
||
out xhLiRun, out xhLiRunLv, out xhMaoLiLv);
|
||
UpdateXianghaoCalc(conn, xhQuoted, x.RowId,
|
||
danTaiChengBen, chengBenJia, danTaiBaoChu, baoChuJia,
|
||
xhLiRun, xhLiRunLv, xhMaoLiLv);
|
||
sumShu += x.ShuLiang;
|
||
sumBC += baoChuJia;
|
||
sumCB += chengBenJia;
|
||
}
|
||
else
|
||
{
|
||
// 未受影响箱号:直接用库里的聚合列值参与图号聚合(之前重算已写好)
|
||
sumShu += x.ShuLiang;
|
||
sumBC += x.BaoChuJia;
|
||
sumCB += x.ChengBenJia;
|
||
}
|
||
}
|
||
|
||
// 图号行聚合 ★ A2:公式统一调 报价公式(总价=单价×台数)
|
||
decimal xiangGuiShuLiang, danJia, zongJia, chengBenZongJia, maoLi, maoLiLv;
|
||
报价公式.图号(sumShu, sumBC, sumCB, taiShu,
|
||
out xiangGuiShuLiang, out danJia, out zongJia, out chengBenZongJia,
|
||
out maoLi, out maoLiLv);
|
||
UpdateTuhaoCalc(conn, thRid, xiangGuiShuLiang, danJia,
|
||
zongJia, chengBenZongJia, maoLi, maoLiLv);
|
||
thAgg[th] = new decimal[] { xiangGuiShuLiang, chengBenZongJia, zongJia };
|
||
}
|
||
catch (Exception exTh)
|
||
{
|
||
Log("[重算·增量] 图号[" + th + "] 异常: " + exTh.Message);
|
||
}
|
||
}
|
||
|
||
// 项目汇总 = 全部图号(未受影响的库聚合值 + 受影响的新值)
|
||
decimal sumPXG = 0m, sumPCB = 0m, sumPZJ = 0m;
|
||
foreach (var kv in thAgg)
|
||
{
|
||
sumPXG += kv.Value[0];
|
||
sumPCB += kv.Value[1];
|
||
sumPZJ += kv.Value[2];
|
||
}
|
||
decimal projLiRun = sumPZJ - sumPCB;
|
||
decimal projLiRunLv = sumPCB == 0m ? 0m : Math.Round(projLiRun / sumPCB, 2);
|
||
UpdateProjectQuoteTotalsById(projectId, sumPXG, sumPCB, sumPZJ, projLiRun, projLiRunLv);
|
||
|
||
sw.Stop();
|
||
Log("[重算·增量] 箱号" + scope.Count + "个 图号" + thGroups.Count + "个 耗时 " + sw.ElapsedMilliseconds + "ms");
|
||
}
|
||
|
||
// 全量分支(原逻辑):scope为空或解析失败;进项目/删改等低频操作走这里
|
||
if (!incremental)
|
||
{
|
||
// 读所有图号行(rowid 顺序)——★ 读"内部ID"列(不是"图号"显示名列),
|
||
// 用户改图号名后,表索引里存的仍是内部ID,用内部ID查才能正确关联箱号表
|
||
var tuhaoRows = new List<TuhaoCalcRow>();
|
||
string selTuhao = "SELECT rowid, 台数, 内部ID FROM " + TuhaoTableName + " ORDER BY rowid;";
|
||
using (var sel = new System.Data.SQLite.SQLiteCommand(selTuhao, conn))
|
||
using (var rdr = sel.ExecuteReader())
|
||
{
|
||
while (rdr.Read())
|
||
{
|
||
var t = new TuhaoCalcRow();
|
||
t.RowId = rdr.GetInt64(0);
|
||
t.TaiShu = ParseDecimal(rdr.IsDBNull(1) ? "" : rdr.GetValue(1));
|
||
t.Tuhao = rdr.IsDBNull(2) ? "" : rdr.GetValue(2).ToString().Trim();
|
||
tuhaoRows.Add(t);
|
||
}
|
||
}
|
||
Log("[重算] 共读取 " + tuhaoRows.Count + " 个图号");
|
||
|
||
decimal sumProjXiangGui = 0m; // 项目:箱柜台数
|
||
decimal sumProjChengBen = 0m; // 项目:成本总价
|
||
decimal sumProjZongJia = 0m; // 项目:报出总价
|
||
|
||
// 遍历每个图号
|
||
foreach (var t in tuhaoRows)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(t.Tuhao)) continue;
|
||
try
|
||
{
|
||
// ★ 复用外层连接查箱号表名(不再各自开新连接)
|
||
string xhTable = GetXianghaoTableByIndex(t.Tuhao, conn);
|
||
if (string.IsNullOrEmpty(xhTable))
|
||
{
|
||
// 没有箱号表 → 图号行的派生列全是 0/空
|
||
UpdateTuhaoCalc(conn, t.RowId, 0m, 0m, 0m, 0m, 0m, 0m);
|
||
Log("[重算] 图号[" + t.Tuhao + "] 无箱号表,派生列置0");
|
||
continue;
|
||
}
|
||
|
||
string xhQuoted = "\"" + xhTable + "\"";
|
||
decimal sumThXiangGuiShuLiang = 0m; // 图号:箱柜数量=SUM(箱号.数量)
|
||
decimal sumThBaoChuJia = 0m; // 图号:单价=SUM(箱号.报出价)
|
||
decimal sumThChengBenJia = 0m; // 图号:成本总价=SUM(箱号.成本价)
|
||
|
||
// ★ 合并:一次 SELECT 同时读 rowid/数量/内部ID(原来分两次查同一张表)
|
||
// ★ 读"内部ID"列(不是"箱号"显示名列),用户改箱号名后表索引仍是内部ID
|
||
var xhRows = new List<XianghaoCalcRow>();
|
||
string selXh = "SELECT rowid, 数量, 内部ID FROM " + xhQuoted + " ORDER BY rowid;";
|
||
using (var sel = new System.Data.SQLite.SQLiteCommand(selXh, conn))
|
||
using (var rdr = sel.ExecuteReader())
|
||
{
|
||
while (rdr.Read())
|
||
{
|
||
var x = new XianghaoCalcRow();
|
||
x.RowId = rdr.GetInt64(0);
|
||
x.ShuLiang = ParseDecimal(rdr.IsDBNull(1) ? "" : rdr.GetValue(1));
|
||
x.Xianghao = rdr.IsDBNull(2) ? "" : rdr.GetValue(2).ToString();
|
||
xhRows.Add(x);
|
||
}
|
||
}
|
||
|
||
// 遍历每个箱号,从其元件表聚合
|
||
foreach (var x in xhRows)
|
||
{
|
||
try
|
||
{
|
||
sumThXiangGuiShuLiang += x.ShuLiang;
|
||
|
||
decimal sumCaiGou = 0m;
|
||
decimal sumBaoChu = 0m;
|
||
// ★ 复用外层连接查元件表名
|
||
string yjTable = GetYuanjianTableByIndex(t.Tuhao, x.Xianghao, conn);
|
||
if (!string.IsNullOrEmpty(yjTable))
|
||
{
|
||
string yjQuoted = "\"" + yjTable + "\"";
|
||
// ★ A2+B2(2026-10-01):元件表重算统一入口(含费用行实时联动)——
|
||
// 普通行按公式重算;费用行按"规格"列公式以最新基数重写表价后再算
|
||
重算元件表聚合含费用联动(conn, yjQuoted, out sumCaiGou, out sumBaoChu);
|
||
}
|
||
|
||
// ★ A2(2026-09-30):公式统一调 报价公式(利润率小数/毛利率百分数值)
|
||
decimal danTaiChengBen, chengBenJia, danTaiBaoChu, baoChuJia, xhLiRun, xhLiRunLv, xhMaoLiLv;
|
||
报价公式.箱号(sumCaiGou, sumBaoChu, x.ShuLiang,
|
||
out danTaiChengBen, out chengBenJia, out danTaiBaoChu, out baoChuJia,
|
||
out xhLiRun, out xhLiRunLv, out xhMaoLiLv);
|
||
|
||
sumThBaoChuJia += baoChuJia;
|
||
sumThChengBenJia += chengBenJia;
|
||
|
||
// UPDATE 箱号行派生列(WHERE rowid)
|
||
UpdateXianghaoCalc(conn, xhQuoted, x.RowId,
|
||
danTaiChengBen, chengBenJia, danTaiBaoChu, baoChuJia,
|
||
xhLiRun, xhLiRunLv, xhMaoLiLv);
|
||
}
|
||
catch (Exception exXh)
|
||
{
|
||
Log("[重算] 图号[" + t.Tuhao + "] 箱号[" + x.Xianghao + "] 异常: " + exXh.Message);
|
||
}
|
||
}
|
||
|
||
// 图号行聚合 ★ A2(2026-09-30):公式统一调 报价公式(总价=单价×台数)
|
||
decimal xiangGuiShuLiang, danJia, zongJia, chengBenZongJia, maoLi, maoLiLv;
|
||
报价公式.图号(sumThXiangGuiShuLiang, sumThBaoChuJia, sumThChengBenJia, t.TaiShu,
|
||
out xiangGuiShuLiang, out danJia, out zongJia, out chengBenZongJia,
|
||
out maoLi, out maoLiLv);
|
||
|
||
sumProjXiangGui += xiangGuiShuLiang;
|
||
sumProjChengBen += chengBenZongJia;
|
||
sumProjZongJia += zongJia;
|
||
|
||
UpdateTuhaoCalc(conn, t.RowId, xiangGuiShuLiang, danJia,
|
||
zongJia, chengBenZongJia, maoLi, maoLiLv);
|
||
Log("[重算] 图号[" + t.Tuhao + "] 箱号" + xhRows.Count + "个 总价=" + zongJia.ToString("F2"));
|
||
}
|
||
catch (Exception exTh)
|
||
{
|
||
Log("[重算] 图号[" + t.Tuhao + "] 异常: " + exTh.Message);
|
||
}
|
||
}
|
||
|
||
// 项目行汇总(写回项目库索引的 5 列)
|
||
decimal projLiRun = sumProjZongJia - sumProjChengBen;
|
||
// ★ 口径(2026-09-29):项目利润率存小数(保留2位,0.05=5%),与箱号表利润率口径一致
|
||
decimal projLiRunLv = sumProjChengBen == 0m ? 0m : Math.Round(projLiRun / sumProjChengBen, 2);
|
||
UpdateProjectQuoteTotalsById(projectId, sumProjXiangGui,
|
||
sumProjChengBen, sumProjZongJia, projLiRun, projLiRunLv);
|
||
|
||
// ★ B3(2026-10-01):全量耗时观测(与增量日志对比,量化 B2 优化效果)
|
||
sw.Stop();
|
||
Log("[重算·全量] 图号" + tuhaoRows.Count + "个 耗时 " + sw.ElapsedMilliseconds + "ms");
|
||
}
|
||
|
||
// ★ B2(2026-10-01):重算的全部写库聚成单事务,在此一次性提交
|
||
recalcTx.Commit();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log("RecalcAllFromDatabase 异常: " + ex.Message);
|
||
}
|
||
|
||
// ★ 进项目/全库重算完成后,同步刷新项目页显示(与 RecalcAllAfterReload 同款收口)
|
||
启动项目页同步();
|
||
|
||
// ★ 修复(2026-09-30):进项目走的是 RecalcAllFromDatabase(不经过 RecalcAllAfterReload),
|
||
// 这里补一次状态栏刷新,否则"项目:"标签不更新(此前一直空白的根因)
|
||
try { 刷新状态栏(); } catch { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// ★ 元件表重算统一入口(含费用行实时联动,2026-10-01):
|
||
/// 第一遍:普通行(类型≠费用)按统一公式(表价×系数×数量)重算四价并累计基数;
|
||
/// 第二遍:费用行——"规格"列存了公式(元件采购价×N% / 元件报出价×N%)的行
|
||
/// 先按【最新基数】重写表价(元件增删改后百分比费用自动跟随,无需重新应用模板),
|
||
/// 再按统一公式算四价;"固定金额"行表价不动只算价。全部计入聚合。
|
||
/// </summary>
|
||
private static void 重算元件表聚合含费用联动(
|
||
System.Data.SQLite.SQLiteConnection conn, string yjQuoted,
|
||
out decimal sumCaiGou, out decimal sumBaoChu)
|
||
{
|
||
sumCaiGou = 0m;
|
||
sumBaoChu = 0m;
|
||
|
||
// ---- 第一遍:普通行 ----
|
||
using (var sel = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT rowid, 表价, 采购系数, 报出系数, 数量 FROM " + yjQuoted +
|
||
" WHERE IFNULL(类型,'')<>'费用' ORDER BY rowid;", conn))
|
||
using (var sr = sel.ExecuteReader())
|
||
{
|
||
while (sr.Read())
|
||
{
|
||
long yjRid = sr.GetInt64(0);
|
||
decimal yjBiaoJia = ParseDecimal(sr.IsDBNull(1) ? "" : sr.GetValue(1));
|
||
decimal yjCgXs = ParseDecimal(sr.IsDBNull(2) ? "" : sr.GetValue(2));
|
||
decimal yjBcXs = ParseDecimal(sr.IsDBNull(3) ? "" : sr.GetValue(3));
|
||
decimal yjSl = ParseDecimal(sr.IsDBNull(4) ? "" : sr.GetValue(4));
|
||
|
||
decimal yjCaiGouDanJia, yjCaiGouJia, yjBaoChuDanJia, yjBaoChuJia;
|
||
报价公式.元件(yjBiaoJia, yjCgXs, yjBcXs, yjSl,
|
||
out yjCaiGouDanJia, out yjCaiGouJia, out yjBaoChuDanJia, out yjBaoChuJia);
|
||
sumCaiGou += yjCaiGouJia;
|
||
sumBaoChu += yjBaoChuJia;
|
||
UpdateYuanjianCalc(conn, yjQuoted, yjRid,
|
||
yjCaiGouDanJia, yjCaiGouJia, yjBaoChuDanJia, yjBaoChuJia);
|
||
}
|
||
}
|
||
|
||
// ---- 第二遍:费用行(联动表价 + 算价 + 计入聚合) ----
|
||
// 公式存专用隐藏列"费用公式";历史数据回退链:费用公式→备注(中间版)→规格(初版),命中即自愈迁移
|
||
EnsureColumns(conn, yjQuoted, new[] { "费用公式" });
|
||
using (var selF = new System.Data.SQLite.SQLiteCommand(
|
||
"SELECT rowid, 费用公式, 备注, 规格, 表价, 采购系数, 报出系数, 数量 FROM " + yjQuoted +
|
||
" WHERE 类型='费用' ORDER BY rowid;", conn))
|
||
using (var srF = selF.ExecuteReader())
|
||
{
|
||
while (srF.Read())
|
||
{
|
||
long rid = srF.GetInt64(0);
|
||
string gf = srF.IsDBNull(1) ? "" : srF.GetValue(1).ToString().Trim();
|
||
string bz = srF.IsDBNull(2) ? "" : srF.GetValue(2).ToString().Trim();
|
||
string gg = srF.IsDBNull(3) ? "" : srF.GetValue(3).ToString().Trim();
|
||
decimal biaoJia = ParseDecimal(srF.IsDBNull(4) ? "" : srF.GetValue(4));
|
||
decimal cgXs = ParseDecimal(srF.IsDBNull(5) ? "" : srF.GetValue(5));
|
||
decimal bcXs = ParseDecimal(srF.IsDBNull(6) ? "" : srF.GetValue(6));
|
||
decimal sl = ParseDecimal(srF.IsDBNull(7) ? "" : srF.GetValue(7));
|
||
|
||
// 公式源三级回退:费用公式列 → 备注 → 规格;命中旧位置则迁移到费用公式列并清掉旧位
|
||
string 公式 = gf;
|
||
bool 需迁移 = false;
|
||
if (公式.IndexOf('×') < 0 && bz.IndexOf('×') >= 0) { 公式 = bz; 需迁移 = true; }
|
||
else if (公式.IndexOf('×') < 0 && gg.IndexOf('×') >= 0) { 公式 = gg; 需迁移 = true; }
|
||
else if (公式 != "固定金额" && bz == "固定金额" && gf == "") { 公式 = bz; 需迁移 = true; }
|
||
|
||
// 公式联动:按公式里的百分比与最新基数重写表价(尾段两头去%,兼容旧"×%13%")
|
||
decimal 新表价 = biaoJia;
|
||
bool 联动 = false;
|
||
string 规范公式 = 公式;
|
||
string 纯百分比 = gg;
|
||
if (公式.StartsWith("元件采购价×", System.StringComparison.Ordinal))
|
||
{
|
||
decimal p = ParseDecimal(公式.Substring("元件采购价×".Length).Trim('%'));
|
||
新表价 = decimal.Round(sumCaiGou * p / 100m, 2);
|
||
联动 = true;
|
||
规范公式 = "元件采购价×" + p.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture) + "%";
|
||
纯百分比 = p.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture) + "%";
|
||
}
|
||
else if (公式.StartsWith("元件报出价×", System.StringComparison.Ordinal))
|
||
{
|
||
decimal p = ParseDecimal(公式.Substring("元件报出价×".Length).Trim('%'));
|
||
新表价 = decimal.Round(sumBaoChu * p / 100m, 2);
|
||
联动 = true;
|
||
规范公式 = "元件报出价×" + p.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture) + "%";
|
||
纯百分比 = p.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture) + "%";
|
||
}
|
||
// 自愈:公式归位到隐藏列"费用公式",规格改纯百分比,备注清空(备注不显示公式,用户要求)
|
||
if (联动 && (需迁移 || 规范公式 != 公式 || 纯百分比 != gg || !string.IsNullOrEmpty(bz)))
|
||
{
|
||
using (var updG = new System.Data.SQLite.SQLiteCommand(
|
||
"UPDATE " + yjQuoted + " SET 费用公式=@g, 规格=@s, 备注='' WHERE rowid=@r;", conn))
|
||
{
|
||
updG.Parameters.AddWithValue("@g", 规范公式);
|
||
updG.Parameters.AddWithValue("@s", 纯百分比);
|
||
updG.Parameters.AddWithValue("@r", rid);
|
||
updG.ExecuteNonQuery();
|
||
}
|
||
}
|
||
if (联动 && 新表价 != biaoJia)
|
||
{
|
||
using (var updB = new System.Data.SQLite.SQLiteCommand(
|
||
"UPDATE " + yjQuoted + " SET 表价=@b WHERE rowid=@r;", conn))
|
||
{
|
||
updB.Parameters.AddWithValue("@b", (double)新表价);
|
||
updB.Parameters.AddWithValue("@r", rid);
|
||
updB.ExecuteNonQuery();
|
||
}
|
||
}
|
||
|
||
decimal cgdj, cgj, bcdj, bcj;
|
||
报价公式.元件(新表价, cgXs, bcXs, sl, out cgdj, out cgj, out bcdj, out bcj);
|
||
sumCaiGou += cgj;
|
||
sumBaoChu += bcj;
|
||
UpdateYuanjianCalc(conn, yjQuoted, rid, cgdj, cgj, bcdj, bcj);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>更新元件行派生列(按 rowid 定位):采购单价/采购价/报出单价/报出价。</summary>
|
||
private static void UpdateYuanjianCalc(System.Data.SQLite.SQLiteConnection conn,
|
||
string yjQuoted, long rowId,
|
||
decimal caiGouDanJia, decimal caiGouJia,
|
||
decimal baoChuDanJia, decimal baoChuJia)
|
||
{
|
||
string sql = "UPDATE " + yjQuoted + " SET " +
|
||
"采购单价=@a, 采购价=@b, 报出单价=@c, 报出价=@d WHERE rowid=@rid;";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
|
||
{
|
||
cmd.Parameters.AddWithValue("@a", caiGouDanJia == 0m ? (object)DBNull.Value : (object)caiGouDanJia);
|
||
cmd.Parameters.AddWithValue("@b", caiGouJia == 0m ? (object)DBNull.Value : (object)caiGouJia);
|
||
cmd.Parameters.AddWithValue("@c", baoChuDanJia == 0m ? (object)DBNull.Value : (object)baoChuDanJia);
|
||
cmd.Parameters.AddWithValue("@d", baoChuJia == 0m ? (object)DBNull.Value : (object)baoChuJia);
|
||
cmd.Parameters.AddWithValue("@rid", rowId);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
|
||
/// <summary>更新箱号行派生列(按 rowid 定位)。所有 decimal 统一转字符串写入(TEXT 列)。</summary>
|
||
private static void UpdateXianghaoCalc(System.Data.SQLite.SQLiteConnection conn,
|
||
string xhQuoted, long rowId,
|
||
decimal danTaiChengBen, decimal chengBenJia, decimal danTaiBaoChu, decimal baoChuJia,
|
||
decimal liRun, decimal liRunLv, decimal maoLiLv)
|
||
{
|
||
string sql = "UPDATE " + xhQuoted + " SET " +
|
||
"单台成本=@a, 成本价=@b, 单台报出=@c, 报出价=@d, " +
|
||
"利润=@e, 利润率=@f, 毛利率=@g WHERE rowid=@rid;";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
|
||
{
|
||
cmd.Parameters.AddWithValue("@a", danTaiChengBen == 0m ? (object)DBNull.Value : (object)danTaiChengBen);
|
||
cmd.Parameters.AddWithValue("@b", chengBenJia == 0m ? (object)DBNull.Value : (object)chengBenJia);
|
||
cmd.Parameters.AddWithValue("@c", danTaiBaoChu == 0m ? (object)DBNull.Value : (object)danTaiBaoChu);
|
||
cmd.Parameters.AddWithValue("@d", baoChuJia == 0m ? (object)DBNull.Value : (object)baoChuJia);
|
||
// ★ 利润三列为 0 是真实计算结果(如 采购系数=报出系数=1 时利润恒 0),
|
||
// 写 0 而不是 NULL——写 NULL 网格显示空白,看起来像"没计算"
|
||
cmd.Parameters.AddWithValue("@e", (object)liRun);
|
||
cmd.Parameters.AddWithValue("@f", liRunLv.ToString());
|
||
cmd.Parameters.AddWithValue("@g", maoLiLv.ToString());
|
||
cmd.Parameters.AddWithValue("@rid", rowId);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
|
||
/// <summary>更新图号行派生列(按 rowid 定位)。</summary>
|
||
private static void UpdateTuhaoCalc(System.Data.SQLite.SQLiteConnection conn,
|
||
long rowId, decimal xiangGuiShuLiang, decimal danJia,
|
||
decimal zongJia, decimal chengBenZongJia, decimal maoLi, decimal maoLiLv)
|
||
{
|
||
string sql = "UPDATE " + TuhaoTableName + " SET " +
|
||
"箱柜数量=@a, 单价=@b, 总价=@c, 成本总价=@d, 毛利=@e, 毛利率=@f " +
|
||
"WHERE rowid=@rid;";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
|
||
{
|
||
cmd.Parameters.AddWithValue("@a", xiangGuiShuLiang == 0m ? (object)DBNull.Value : (object)xiangGuiShuLiang);
|
||
cmd.Parameters.AddWithValue("@b", danJia == 0m ? (object)DBNull.Value : (object)danJia);
|
||
cmd.Parameters.AddWithValue("@c", zongJia == 0m ? (object)DBNull.Value : (object)zongJia);
|
||
cmd.Parameters.AddWithValue("@d", chengBenZongJia == 0m ? (object)DBNull.Value : (object)chengBenZongJia);
|
||
cmd.Parameters.AddWithValue("@e", maoLi == 0m ? (object)DBNull.Value : (object)maoLi);
|
||
cmd.Parameters.AddWithValue("@f", maoLiLv.ToString());
|
||
cmd.Parameters.AddWithValue("@rid", rowId);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
|
||
/// <summary>按项目编号把 5 个汇总列 UPDATE 回项目库索引(独立连接,不依赖 _currentProjectId)。</summary>
|
||
private void UpdateProjectQuoteTotalsById(string projectId,
|
||
decimal xiangGuiTaiShu, decimal chengBenZongJia,
|
||
decimal baoChuZongJia, decimal liRun, decimal liRunLv)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(projectId)) return;
|
||
string dbPath = GetDbFullPath(ProjectsDbPath);
|
||
if (!System.IO.File.Exists(dbPath)) return;
|
||
try
|
||
{
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
string sql = "UPDATE " + ProjectsTableName + " SET " +
|
||
"箱柜台数=@xgts, 成本总价=@cbzj, 报出总价=@bczj, " +
|
||
"利润=@lr, 利润率=@lrl WHERE 项目编号=@id;";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
|
||
{
|
||
cmd.Parameters.AddWithValue("@xgts", xiangGuiTaiShu == 0m ? (object)DBNull.Value : (object)xiangGuiTaiShu);
|
||
cmd.Parameters.AddWithValue("@cbzj", chengBenZongJia == 0m ? (object)DBNull.Value : (object)chengBenZongJia);
|
||
cmd.Parameters.AddWithValue("@bczj", baoChuZongJia == 0m ? (object)DBNull.Value : (object)baoChuZongJia);
|
||
cmd.Parameters.AddWithValue("@lr", liRun == 0m ? (object)DBNull.Value : (object)liRun);
|
||
cmd.Parameters.AddWithValue("@lrl", liRunLv.ToString());
|
||
cmd.Parameters.AddWithValue("@id", projectId);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log("UpdateProjectQuoteTotalsById 异常: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
// 全量重算用的临时数据结构
|
||
private class TuhaoCalcRow
|
||
{
|
||
public long RowId;
|
||
public string Tuhao = "";
|
||
public decimal TaiShu;
|
||
}
|
||
private class XianghaoCalcRow
|
||
{
|
||
public long RowId;
|
||
public string Xianghao = "";
|
||
public decimal ShuLiang;
|
||
public decimal BaoChuJia;
|
||
public decimal ChengBenJia;
|
||
}
|
||
|
||
// ---------- 项目表重算(聚合当前项目所有图号,第 4 级) ----------
|
||
|
||
/// <summary>
|
||
/// 聚合当前项目(advancedDataGridView9 当前显示的所有图号行),
|
||
/// 重算项目表(advancedDataGridView6)当前项目的 5 个汇总列:
|
||
/// 箱柜台数 = SUM(所有图号.箱柜数量)
|
||
/// 成本总价 = SUM(所有图号.成本总价)
|
||
/// 报出总价 = SUM(所有图号.总价)
|
||
/// 利润 = 报出总价 − 成本总价
|
||
/// 利润率 = (成本总价=0 ? 0 : 利润 / 成本总价)
|
||
/// 对应 Excel 项目表公式 H2=图号表!E8、I2=图号表!H8、J2=图号表!G8、K2=J2−I2、L2=IF(I2=0,0,K2/I2)。
|
||
/// 同时立即 UPDATE 回项目数据库,保证下次启动仍显示正确。
|
||
/// </summary>
|
||
private void RecalcProjectRowFromTuhao()
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
|
||
|
||
// 聚合当前项目所有图号行的 箱柜数量/成本总价/总价
|
||
decimal sumXiangGuiShuLiang = 0m;
|
||
decimal sumChengBenZongJia = 0m;
|
||
decimal sumZongJia = 0m;
|
||
DataGridView dgv9 = this.advancedDataGridView9;
|
||
if (dgv9 != null && dgv9.Rows != null)
|
||
{
|
||
foreach (DataGridViewRow th in dgv9.Rows)
|
||
{
|
||
if (th == null || th.IsNewRow) continue;
|
||
sumXiangGuiShuLiang += GetCellDecimal(th, "箱柜数量");
|
||
sumChengBenZongJia += GetCellDecimal(th, "成本总价");
|
||
sumZongJia += GetCellDecimal(th, "总价");
|
||
}
|
||
}
|
||
|
||
decimal xiangGuiTaiShu = sumXiangGuiShuLiang; // 箱柜台数
|
||
decimal baoChuZongJia = sumZongJia; // 报出总价
|
||
decimal liRun = baoChuZongJia - sumChengBenZongJia; // 利润
|
||
// ★ 口径(2026-09-29):利润率存小数(保留2位,0.05=5%)
|
||
decimal liRunLv = sumChengBenZongJia == 0m
|
||
? 0m : Math.Round(liRun / sumChengBenZongJia, 2); // 利润率
|
||
|
||
// 更新项目表显示(只改内存,不写库;留给保存按钮/离页触发统一写库)
|
||
DataGridViewRow projRow = FindProjectRowById(_currentProjectId);
|
||
if (projRow != null)
|
||
{
|
||
SetCell(projRow, "箱柜台数", xiangGuiTaiShu);
|
||
SetCell(projRow, "成本总价", sumChengBenZongJia);
|
||
SetCell(projRow, "报出总价", baoChuZongJia);
|
||
SetCell(projRow, "利润", liRun);
|
||
SetCell(projRow, "利润率", liRunLv);
|
||
}
|
||
|
||
// 不再立即写回项目数据库——编辑单元格只改内存,
|
||
// 库里的项目汇总在点保存按钮/离开报价页时由 SaveAllToDatabase 重算写入。
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把项目表的 5 个汇总列 UPDATE 回项目数据库。
|
||
/// (项目表没有主键,但"项目编号"可作为业务键定位单行。)
|
||
/// </summary>
|
||
private void UpdateProjectQuoteTotals(decimal xiangGuiTaiShu,
|
||
decimal chengBenZongJia, decimal baoChuZongJia,
|
||
decimal liRun, decimal liRunLv)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(_currentProjectId)) return;
|
||
string dbPath = GetDbFullPath(ProjectsDbPath);
|
||
if (!System.IO.File.Exists(dbPath)) return;
|
||
|
||
try
|
||
{
|
||
using (var conn = new System.Data.SQLite.SQLiteConnection(GetConnString(dbPath)))
|
||
{
|
||
conn.Open();
|
||
string sql = "UPDATE " + ProjectsTableName + " SET " +
|
||
"箱柜台数 = @xgts, 成本总价 = @cbzj, 报出总价 = @bczj, " +
|
||
"利润 = @lr, 利润率 = @lrl " +
|
||
"WHERE 项目编号 = @id;";
|
||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
|
||
{
|
||
cmd.Parameters.AddWithValue("@xgts", xiangGuiTaiShu.ToString());
|
||
cmd.Parameters.AddWithValue("@cbzj", chengBenZongJia.ToString());
|
||
cmd.Parameters.AddWithValue("@bczj", baoChuZongJia.ToString());
|
||
cmd.Parameters.AddWithValue("@lr", liRun.ToString());
|
||
cmd.Parameters.AddWithValue("@lrl", liRunLv.ToString());
|
||
cmd.Parameters.AddWithValue("@id", _currentProjectId);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 存库失败不阻断界面重算(界面值已正确更新)
|
||
}
|
||
}
|
||
|
||
// ---------- CellValueChanged 事件处理器 ----------
|
||
|
||
/// <summary>编辑单元格时立即提交,触发 CellValueChanged。</summary>
|
||
/// <summary>记录最后一次重算结束的时间,用于 Grid_DirtyStateChanged 里过滤掉
|
||
/// 重算后表格刷新导致的“假变脏”。重算结束后短时间内的变脏不提交,避免清空用户输入。</summary>
|
||
private DateTime _lastRecalcTime = DateTime.MinValue;
|
||
|
||
private void Grid_DirtyStateChanged(object sender, EventArgs e)
|
||
{
|
||
// 程序代码主动改值(重算/加载)时不提交,避免触发 CellValueChanged 形成死循环
|
||
if (_isRecalculating || _isReloading) return;
|
||
|
||
// ★ 关键修复:重算刚结束后的短时间内的变脏不提交。
|
||
// 原因:重算写公式列 → 表格重绘 → 正在编辑的单元格编辑控件被重置 → 变脏 →
|
||
// 提交残留值覆盖原始值 → 表价被清空 → 只能输入一个数字。
|
||
// 时间窗口设为 2 秒,足够覆盖重算+重绘的完整周期。
|
||
var msSinceRecalc = (DateTime.Now - _lastRecalcTime).TotalMilliseconds;
|
||
if (msSinceRecalc < 2000) return;
|
||
|
||
var dgv = sender as DataGridView;
|
||
if (dgv != null && dgv.IsCurrentCellDirty)
|
||
{
|
||
string colName = dgv.CurrentCell.OwningColumn != null
|
||
? dgv.CurrentCell.OwningColumn.HeaderText : "";
|
||
Log("[Dirty] " + dgv.Name + " 行" + dgv.CurrentCell.RowIndex
|
||
+ " 列[" + colName + "] → CommitEdit (距上次重算" + (int)msSinceRecalc + "ms)");
|
||
dgv.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||
// ★ 直写库架构:编辑即写库,不再有"未保存"状态
|
||
}
|
||
}
|
||
|
||
/// <summary>元件表单元格变化。
|
||
/// ★★ 一劳永逸(2026-09-30 定稿):本事件的重算链已【整体废弃】——
|
||
/// 虚拟模式编辑提交会同时触发 CellValuePushed(已统一处理:本行公式+防抖全库重算+从库刷新)
|
||
/// 和本事件;这里若再用 GetFirstSelectedRow(选中行)做聚合,箱号表多选残留时
|
||
/// 会把合计写到错误的箱号行——这是"价格先跳错值再纠正"的真正根因(潜伏至今)。
|
||
/// 本行公式已由 CellValuePushed 计算,这里只留诊断日志。</summary>
|
||
private void Yuanjian_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||
{
|
||
if (_isRecalculating || _isReloading) return;
|
||
if (e.RowIndex < 0) return;
|
||
var dgv = this.advancedDataGridView5;
|
||
if (dgv == null || e.RowIndex >= dgv.Rows.Count) return;
|
||
var row = dgv.Rows[e.RowIndex];
|
||
if (row == null || row.IsNewRow) return;
|
||
|
||
// 取列名和值,用于日志
|
||
string colName = "";
|
||
object oldVal = null;
|
||
if (e.ColumnIndex >= 0 && e.ColumnIndex < dgv.Columns.Count)
|
||
{
|
||
colName = dgv.Columns[e.ColumnIndex].HeaderText;
|
||
oldVal = row.Cells[e.ColumnIndex].Value;
|
||
}
|
||
Log("[ValueChanged] 元件表 行" + e.RowIndex + " 列[" + colName + "] 值=" + (oldVal ?? "(null)") +
|
||
" (聚合已统一走防抖全库重算,本事件不再写箱号/图号)");
|
||
}
|
||
|
||
/// <summary>箱号表单元格变化。
|
||
/// ★★ 一劳永逸(2026-09-30 定稿):聚合链废弃——箱号/图号派生列统一交给
|
||
/// 防抖全库重算+从库刷新(与元件编辑同链),本事件只处理改名日志。</summary>
|
||
private void Xianghao_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||
{
|
||
if (_isRecalculating || _isReloading) return;
|
||
if (e.RowIndex < 0) return;
|
||
var dgv = this.advancedDataGridView4;
|
||
if (dgv == null || e.RowIndex >= dgv.Rows.Count) return;
|
||
var row = dgv.Rows[e.RowIndex];
|
||
if (row == null || row.IsNewRow) return;
|
||
|
||
// ★ 用户改"箱号"列(显示名):只改表格显示,不更新表索引和 _currentXianghao(用内部ID)
|
||
// 这样即使两个箱号改成同名,数据库表索引里"图号+箱号(内部ID)"也不会重复
|
||
// _currentXianghao 始终是内部ID(从"内部ID"列读取),与表索引一致
|
||
DataGridViewColumn xhCol = e.ColumnIndex >= 0 ? dgv.Columns[e.ColumnIndex] : null;
|
||
string xhColName = xhCol == null ? "" :
|
||
(!string.IsNullOrEmpty(xhCol.DataPropertyName) ? xhCol.DataPropertyName : xhCol.HeaderText);
|
||
if (xhColName == "箱号")
|
||
{
|
||
string newXhName = GetCellValueAsString(row, "箱号");
|
||
Log("[改名] 箱号显示名改为: " + newXhName + " (内部ID不变,表索引不变)");
|
||
// 不调用 UpdateXianghaoIndex,不更新 _currentXianghao
|
||
}
|
||
// 聚合(图号行)不再在此计算——统一走防抖全库重算(防选中残留错位)
|
||
}
|
||
|
||
/// <summary>图号表单元格变化:仅台数列变化时重算该行(总价/成本总价/毛利依赖台数)。</summary>
|
||
private void Tuhao_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||
{
|
||
if (_isRecalculating || _isReloading) return;
|
||
if (e.RowIndex < 0) return;
|
||
var dgv = this.advancedDataGridView9;
|
||
if (dgv == null || e.RowIndex >= dgv.Rows.Count) return;
|
||
var row = dgv.Rows[e.RowIndex];
|
||
if (row == null || row.IsNewRow) return;
|
||
|
||
DataGridViewColumn col = e.ColumnIndex >= 0 ? dgv.Columns[e.ColumnIndex] : null;
|
||
string colName = col == null ? "" :
|
||
(!string.IsNullOrEmpty(col.DataPropertyName) ? col.DataPropertyName : col.HeaderText);
|
||
|
||
// ★ 用户改"图号"列(显示名):只改表格显示,不更新表索引和 _currentTuhao(用内部ID)
|
||
// 这样即使两个图号改成同名,数据库表索引里"图号(内部ID)"也不会重复
|
||
// _currentTuhao 始终是内部ID(从"内部ID"列读取),与表索引一致
|
||
if (colName == "图号")
|
||
{
|
||
string newThName = GetCellValueAsString(row, "图号");
|
||
Log("[改名] 图号显示名改为: " + newThName + " (内部ID不变,表索引不变)");
|
||
// 不调用 UpdateTuhaoIndex,不更新 _currentTuhao
|
||
return;
|
||
}
|
||
|
||
// 只在"台数"列变化时才重算
|
||
if (colName != "台数") return;
|
||
|
||
// ★★ 一劳永逸(2026-09-30 定稿):图号行派生列统一交给防抖全库重算+从库刷新
|
||
// (与元件/箱号编辑同链),本事件不再直接计算(防选中残留错位)
|
||
Log("[ValueChanged] 图号表 行" + e.RowIndex + " 列[" + colName + "] (聚合统一走防抖全库重算)");
|
||
合并视图防抖重算();
|
||
}
|
||
|
||
// ============================================================
|
||
// SelectionChanged 联动事件
|
||
// ============================================================
|
||
|
||
/// <summary>图号表选中变化:更新当前图号,联动刷新箱号表。
|
||
/// ★ 多选联动(2026-09-30):多选图号时箱号表显示多图号箱号合集。</summary>
|
||
private void advancedDataGridView9_SelectionChanged(object sender, EventArgs e)
|
||
{
|
||
if (_isReloading) return;
|
||
// ★ 联动改用"当前行"(用户最后点击的行):多选复制后残留的选择不再阻断联动,
|
||
// 单击/多选状态下点击行都会刷新箱号表(用户要求点击图号自动跳转)
|
||
|
||
DataGridViewRow row = this.advancedDataGridView9.CurrentRow;
|
||
if (row == null || row.IsNewRow) return;
|
||
|
||
string tuhao = GetBoundValueAsString(row, "内部ID"); // ★ 用内部ID,用户改名不影响
|
||
if (string.IsNullOrWhiteSpace(tuhao)) return;
|
||
|
||
// ★ 多选:收集全部选中图号(行序;CurrentRow 保底)
|
||
var thList = new System.Collections.Generic.List<string>();
|
||
foreach (DataGridViewRow r in this.advancedDataGridView9.Rows)
|
||
{
|
||
if (r == null || r.IsNewRow || !r.Selected) continue;
|
||
string iid = GetBoundValueAsString(r, "内部ID");
|
||
if (!string.IsNullOrWhiteSpace(iid) && !thList.Contains(iid)) thList.Add(iid);
|
||
}
|
||
if (!thList.Contains(tuhao)) thList.Insert(0, tuhao);
|
||
|
||
bool changed = tuhao != _currentTuhao;
|
||
if (!changed && thList.Count <= 1) return; // 没变化,不重复刷新
|
||
|
||
_currentTuhao = tuhao; // CurrentRow 语义保持(插入/新建等功能不受影响)
|
||
if (thList.Count > 1)
|
||
ReloadXianghaoByTuhaos(_currentProjectId, thList); // ★ 多图号合并视图
|
||
else
|
||
ReloadXianghaoByTuhao(_currentProjectId, _currentTuhao); // 单选:原路径(含从合并视图切回)
|
||
}
|
||
|
||
/// <summary>箱号表选中变化:更新当前箱号,联动刷新元件表。
|
||
/// ★ 多选联动(2026-09-30):多选箱号(或合并视图)时元件表显示多箱号元件合集。</summary>
|
||
private void advancedDataGridView4_SelectionChanged(object sender, EventArgs e)
|
||
{
|
||
if (_isReloading) return;
|
||
// ★ 联动改用"当前行"(用户最后点击的行):多选复制后残留的选择不再阻断联动
|
||
|
||
DataGridViewRow row = this.advancedDataGridView4.CurrentRow;
|
||
if (row == null || row.IsNewRow) return;
|
||
|
||
string xh = GetBoundValueAsString(row, "内部ID"); // ★ 用内部ID,用户改名不影响
|
||
if (string.IsNullOrWhiteSpace(xh)) return;
|
||
|
||
// 合并视图判定(_来源图号_ 列是合并表标记)
|
||
bool mergedView = _xianghaoTable != null &&
|
||
string.IsNullOrEmpty(_xianghaoTable.TableName) &&
|
||
_xianghaoTable.Columns.Contains("_来源图号_");
|
||
|
||
// ★ 多选:收集选中行(行序去重)
|
||
var rowSet = new System.Collections.Generic.SortedSet<int>();
|
||
foreach (DataGridViewCell c in this.advancedDataGridView4.SelectedCells)
|
||
{
|
||
if (c != null && c.RowIndex >= 0) rowSet.Add(c.RowIndex);
|
||
}
|
||
if (!rowSet.Contains(row.Index)) rowSet.Add(row.Index);
|
||
|
||
bool multi = rowSet.Count > 1;
|
||
if (!multi && xh == _currentXianghao && !mergedView) return; // 没变化,不重复刷新
|
||
|
||
_currentXianghao = xh; // CurrentRow 语义保持
|
||
|
||
if (multi)
|
||
{
|
||
// ★ 三元组:来源图号 / 箱号内部ID / 箱号显示名(合并视图序号列显示"箱号-N"用)
|
||
var pairs = new System.Collections.Generic.List<string[]>();
|
||
foreach (int idx in rowSet)
|
||
{
|
||
if (_xianghaoTable == null || idx >= _xianghaoTable.Rows.Count) continue;
|
||
string thSrc = mergedView
|
||
? Convert.ToString(_xianghaoTable.Rows[idx]["_来源图号_"])
|
||
: _currentTuhao;
|
||
string xhId = Convert.ToString(_xianghaoTable.Rows[idx]["内部ID"]);
|
||
string xhName = Convert.ToString(_xianghaoTable.Rows[idx]["箱号"]);
|
||
if (!string.IsNullOrWhiteSpace(xhId))
|
||
pairs.Add(new[] { thSrc, xhId, xhName });
|
||
}
|
||
ReloadYuanjianByXianghaos(_currentProjectId, pairs); // ★ 多箱号合并视图
|
||
}
|
||
else if (mergedView)
|
||
{
|
||
// 合并视图下单选:按该行来源图号查元件(不能再用 _currentTuhao)
|
||
int idx = row.Index;
|
||
string thSrc = (_xianghaoTable != null && idx < _xianghaoTable.Rows.Count)
|
||
? Convert.ToString(_xianghaoTable.Rows[idx]["_来源图号_"]) : _currentTuhao;
|
||
ReloadYuanjianByXianghao(_currentProjectId, thSrc, xh);
|
||
}
|
||
else
|
||
{
|
||
ReloadYuanjianByXianghao(_currentProjectId, _currentTuhao, xh); // 原路径
|
||
}
|
||
}
|
||
|
||
/// <summary>元件表选中变化:仅记录,无下级可联动。</summary>
|
||
private void advancedDataGridView5_SelectionChanged(object sender, EventArgs e)
|
||
{
|
||
// 元件是最末级,无下级联动
|
||
}
|
||
|
||
/// <summary>★ 治本(2026-09-30):按"内部ID"在表格中找行(数据归属定位,与视觉选中解耦)。
|
||
/// 找不到返回 null。用于编辑后聚合——目标行永远是"数据所属的那一行",
|
||
/// 不受多选残留/选中顺序影响。</summary>
|
||
private DataGridViewRow 找行按内部ID(DataGridView dgv, string 内部ID)
|
||
{
|
||
if (dgv == null || string.IsNullOrWhiteSpace(内部ID) || dgv.Rows == null) return null;
|
||
foreach (DataGridViewRow r in dgv.Rows)
|
||
{
|
||
if (r == null || r.IsNewRow) continue;
|
||
if (string.Equals(GetBoundValueAsString(r, "内部ID"), 内部ID, StringComparison.OrdinalIgnoreCase))
|
||
return r;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>通用:取表格当前选中第一行(排除末尾新行)。
|
||
/// ★ 修复(2026-09-30):SelectedRows/SelectedCells 集合按"选中操作逆序"排列
|
||
/// (最后点选的排最前)——原实现遍历取首个 = 最后点击的行,多选时聚合目标行不稳定
|
||
/// (如先选AH4再Ctrl加选AH3,聚合会写进AH3)。改为取行索引最小的选中行(真正的第一行)。</summary>
|
||
private static DataGridViewRow GetFirstSelectedRow(DataGridView dgv)
|
||
{
|
||
if (dgv == null || dgv.Rows == null || dgv.Rows.Count == 0) return null;
|
||
int minIdx = int.MaxValue;
|
||
DataGridViewRow best = null;
|
||
// 从选中单元格取最小行索引(CellSelect/RowHeaderSelect 两模式都覆盖)
|
||
if (dgv.SelectedCells != null)
|
||
{
|
||
foreach (DataGridViewCell c in dgv.SelectedCells)
|
||
{
|
||
if (c == null || c.RowIndex < 0 || c.RowIndex >= minIdx) continue;
|
||
var r = dgv.Rows[c.RowIndex];
|
||
if (r == null || r.IsNewRow) continue;
|
||
minIdx = c.RowIndex;
|
||
best = r;
|
||
}
|
||
}
|
||
if (best != null) return best;
|
||
// 兜底:没有任何选中单元格时用当前行
|
||
if (dgv.CurrentRow != null && !dgv.CurrentRow.IsNewRow)
|
||
{
|
||
return dgv.CurrentRow;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取表格所有选中的行(支持批量多选,CellSelect 模式下从 SelectedCells 提取去重行)。
|
||
/// 至少返回当前行(如果没有任何选中)。
|
||
/// </summary>
|
||
private static System.Collections.Generic.List<DataGridViewRow> GetAllSelectedRows(DataGridView dgv)
|
||
{
|
||
var result = new System.Collections.Generic.List<DataGridViewRow>();
|
||
if (dgv == null || dgv.Rows == null || dgv.Rows.Count == 0) return result;
|
||
|
||
// 用行索引去重(CellSelect 下同一行可能有多个单元格被选中)
|
||
var seenRows = new System.Collections.Generic.HashSet<int>();
|
||
|
||
// 1) 从 SelectedRows 取(RowHeaderSelect 模式下有效)
|
||
if (dgv.SelectedRows != null)
|
||
{
|
||
foreach (DataGridViewRow r in dgv.SelectedRows)
|
||
{
|
||
if (r == null || r.IsNewRow) continue;
|
||
if (seenRows.Add(r.Index)) result.Add(r);
|
||
}
|
||
}
|
||
|
||
// 2) 从 SelectedCells 提取行(CellSelect 模式下 SelectedRows 可能不全,
|
||
// 必须无条件执行,不能只在 result.Count==0 时才执行)
|
||
if (dgv.SelectedCells != null && dgv.SelectedCells.Count > 0)
|
||
{
|
||
foreach (DataGridViewCell cell in dgv.SelectedCells)
|
||
{
|
||
if (cell == null || cell.RowIndex < 0) continue;
|
||
DataGridViewRow r = dgv.Rows[cell.RowIndex];
|
||
if (r == null || r.IsNewRow) continue;
|
||
if (seenRows.Add(r.Index)) result.Add(r);
|
||
}
|
||
}
|
||
|
||
// 3) 都没有就兜底用 CurrentRow
|
||
if (result.Count == 0 && dgv.CurrentRow != null && !dgv.CurrentRow.IsNewRow)
|
||
{
|
||
result.Add(dgv.CurrentRow);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>从表格当前选中行读取某列值,并更新到外部状态字段。</summary>
|
||
private static void SyncStateFromGrid(DataGridView dgv, string colName, ref string state)
|
||
{
|
||
DataGridViewRow row = GetFirstSelectedRow(dgv);
|
||
if (row == null) return;
|
||
state = GetCellValueAsString(row, colName);
|
||
}
|
||
|
||
}
|
||
}
|