Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/hp_bid_xlsx.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Three distinct values: **`Part Number`**, **`Bundle`**, **`Bundle Detail`**.

| Field | `Part Number` / `Bundle` | `Bundle Detail` |
|---|---|---|
| **Line sequence** → Item col A | Increment top-level counter (`1`, `2`, …). For `Bundle`, also reset child counter. | `{parentSeq}.{childCounter:D2}` (`1.01`, `1.02`, …) |
| **Line sequence** → Item col A | Next number in one running sequence (`1`, `2`, `3`, …). | Next number in the same running sequence (no longer sub-numbered under the Bundle). |
| **vpn** | `Product Number/ID`; if `Option Code` non-empty: `"Product Number/ID#Option Code"` | same rule |
| **description** | `Product Description` | `Product Description` |
| **cost** | `Price` (default 0 when blank) | **0** — the component price is dropped (the Bundle parent line carries the total). Source `Price` is still kept in `Raw["Price"]`. |
Expand Down
4 changes: 2 additions & 2 deletions docs/hp_oneconfig_xlsx.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Iterate rows below the Components header. Stop at the first wholly-empty row (or

For each kept row, emit a child `LineItem`:

- `LineSequence = "1.NN"` where `NN` is a two-digit zero-padded counter (`1.01`, `1.02`, …, `1.30`)
- `LineSequence` continues the single running sequence after the parent (`2`, `3`, …, `31`); components are no longer sub-numbered as `1.NN`
- `vpn` ← `Part Number` (trimmed)
- `description` ← `Description` (trimmed)
- `qty` ← `Quantity` → `DecimalCleaner.ParseOptionalInt` (defaults to 1)
Expand All @@ -67,7 +67,7 @@ Writer: `PercentOffWithUpliftWriter`. Column mapping:

| Col | Header | Value |
|---|---|---|
| A | `Item` | `LineSequence` (`1`, `1.01`, …) |
| A | `Item` | `LineSequence` (`1`, `2`, `3`, …) |
| B | `Vendor Name` | `HP` |
| D | `Vendor Part Number` | `vpn` |
| E | `Description` | `description` |
Expand Down
11 changes: 6 additions & 5 deletions docs/hpe_bid_xlsx.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Three distinct values: **`Part Number`**, **`Bundle`**, **`BundleDetails`** (one

| Field | `Part Number` | `Bundle` | `BundleDetails` |
|---|---|---|---|
| **Line sequence** → Item col A | top-level counter (`1`, `2`, …) | top-level counter; also opens a child group (reset child counter) | `{parentSeq}.{childCounter:D2}` (`1.01`, `1.02`, …) |
| **Line sequence** → Item col A | next number in one running sequence (`1`, `2`, `3`, …) | next number in the same running sequence | next number in the same running sequence (no longer sub-numbered as `parent.NN`) |
| **vpn** | `ProductNumber` | `BundleID` | `ComponentID` |
| **description** | `ProductDescription` | `ProductDescription` | `ProductDescription` |
| **msrp** | `ListPrcEst` (default 0 when blank) | `ListPrcEst` | **0** — component msrp dropped (parent carries it) |
Expand Down Expand Up @@ -121,11 +121,12 @@ which is **not** appended.
| Seq | Line Type | VPN | MSRP | Cost | Qty | Comments |
|---|---|---|---|---|---|---|
| 1 | Bundle | 52080474 | 87995.00 | 26632.75 | 1 | Max Qty: 1 |
| 1.01 | BundleDetails | AK379B | 0 → `0.0001` | 0 → `0.0001` | 1 | _(blank)_ |
| 1.02 | BundleDetails | R6Q75A | 0 → `0.0001` | 0 → `0.0001` | 2 | _(blank)_ |
| 2 | BundleDetails | AK379B | 0 → `0.0001` | 0 → `0.0001` | 1 | _(blank)_ |
| 3 | BundleDetails | R6Q75A | 0 → `0.0001` | 0 → `0.0001` | 2 | _(blank)_ |
| … | … | … | … | … | … | … |
| 2 | Bundle | 52079278 | 320784.00 | 58842.80 | 1 | Max Qty: 1 |
| 3 | Bundle | 52079277 | 299357.36 | 46237.81 | 1 | Max Qty: 1 |
| 11 | Bundle | 52079278 | 320784.00 | 58842.80 | 1 | Max Qty: 1 |
| … | … | … | … | … | … | … |
| 23 | Bundle | 52079277 | 299357.36 | 46237.81 | 1 | Max Qty: 1 |

Bundle `vpn` comes from `BundleID` (the numeric bundle id), not the row's `ProductNumber`.
Computed total: `131713.36` (Σ Bundle cost × qty; BundleDetails contribute 0; no quoted total).
Expand Down
4 changes: 2 additions & 2 deletions docs/lenovo_brda_dcg_pdf.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ Scan for `"Quote"` followed by a word starting `"No"` on the same y-band (≤ 5

### Assembly

1. Iterate section 1 entries in order, assigning a global monotonically increasing sequence number.
1. Iterate section 1 entries in order, assigning every emitted line the next number in a single running sequence (`1`, `2`, `3`, …).
2. CONFIG entries → top-level item; description = config's own VPN.
3. PARENT entries → top-level item; look up `children[entry.LineNo]` from the section 2 results and emit each child with sequence `"{parentSeq}.{childIdx:D2}"`.
3. PARENT entries → top-level item; look up `children[entry.LineNo]` from the section 2 results and emit each child as the next number in the same running sequence (children are no longer sub-numbered as `parent.NN`).

---

Expand Down
2 changes: 1 addition & 1 deletion docs/lenovo_brda_dcg_xlsx.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The footer carries `Total:` in the unit-price column and the AUD total in the ex

## Line sequence numbering

Parents are numbered as integers from 1 in document order. Children are numbered `parent.NN` where `NN` is the 1-based index of the child within its parent (`5.01`, `5.02`, …). `LineSequence` is a string — `AnzGenericWriter` writes it to column A verbatim.
Every emitted line — parent and child alike — takes the next number in a single running sequence (`1`, `2`, `3`, …) in document order; children are no longer sub-numbered as `parent.NN`. Because the sequence no longer encodes the hierarchy, parent vs child is distinguished by cost (parents carry the real unit price; children carry the dropped 0). `LineSequence` is a string — `AnzGenericWriter` writes it to column A verbatim.

## Edge cases the tests cover

Expand Down
8 changes: 4 additions & 4 deletions docs/output_mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ HP writes to the **local** columns of the 27-column layout. No FX rate, no forei

| Col | Header | HP value | Notes |
|---|---|---|---|
| A | Item | `LineSequence` | String: `"1"`, `"2"`, `"1.01"`, `"1.02"`, … |
| A | Item | `LineSequence` | String: `"1"`, `"2"`, `"3"`, … — single running sequence across Part Number, Bundle, and Bundle Detail lines |
| B | Vendor Name | `"HP"` | Upper-case vendor label |
| D | Vendor Part Number | `vpn` | `"Product Number/ID"` or `"Product Number/ID#Option Code"` |
| E | Description | `description` | `"Product Description"` from the source row |
Expand All @@ -112,7 +112,7 @@ All other columns: blank.
**End-loop sentinel row:** after the last line item — col B = `"*"`, col D = `EndLoopWarning` constant.

**Key differences vs ForeignUplift:**
- Item col A holds the `LineSequence` string (`"1.01"` etc.) rather than a running integer
- Item col A holds the `LineSequence` string — a single running sequence (`"1"`, `"2"`, `"3"`, …) across every line type
- Zero costs export as the `0.0001` sentinel in column I (every Bundle Detail, whose price is dropped onto its Bundle parent); non-zero Part Number / Bundle costs are written as-is
- Comments col R holds `"Max Qty: {Max Deal Qty}"` for Part Number / Bundle lines (the deal quantity, no longer used as Qty); blank for Bundle Detail
- No term, date, serial number, or FX columns populated
Expand All @@ -130,7 +130,7 @@ Same 27-column ANZ-GENERIC layout. Differences from HP Bid:

| Col | Header | HPE value | Notes |
|---|---|---|---|
| A | Item | `LineSequence` | `"1"`, `"2"`, `"1.01"`, … (Bundle opens a child group; `BundleDetails` nest as `parent.NN`) |
| A | Item | `LineSequence` | `"1"`, `"2"`, `"3"`, … — single running sequence across Bundle and BundleDetails lines |
| B | Vendor Name | `"HPE"` | |
| D | Vendor Part Number | `vpn` | `ProductNumber` (Part Number) / `BundleID` (Bundle) / `ComponentID` (BundleDetails). **`OptionCode` is not appended.** |
| E | Description | `description` | `ProductDescription` |
Expand Down Expand Up @@ -184,7 +184,7 @@ Used by **HP OneConfig (XLSX)** only. Unlike the No Calculation / Uplift writers

| Col | Header | OneConfig value | Notes |
|---|---|---|---|
| A | Item | `LineSequence` | `"1"` (parent), `"1.01"`, `"1.02"`, … (children) |
| A | Item | `LineSequence` | `"1"` (parent), then `"2"`, `"3"`, … (children) |
| B | Vendor Name | `"HP"` | Upper-case vendor label |
| D | Vendor Part Number | `vpn` | Parent: `Config ID`; children: `Part Number` |
| E | Description | `description` | Parent: `Config Name`; children: source `Description` |
Expand Down
2 changes: 1 addition & 1 deletion docs/project_memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ Out of scope: multi-file batch upload, CSV formats, review/approve gate, multi-t
- `docs/hp_global_bid_xlsx.md` — HP Global Bid (XLSX). `Product numbers` sheet; header anchor `"Product number"`; AUD-only (`Converted net price [AUD]` column required, else `ParseError("currency")`); qty always 1; comments encode remaining qty (`"{remaining} Remaining"`; `Full term (Months)` no longer parsed); no quoted total; available templates `No Calculation` and `Uplift`.
- `docs/hp_oneconfig_xlsx.md` — HP OneConfig (XLSX). Single Config per file, parent VPN from `Config ID`, MSRP from `Total Price`, 30 children zeroed. Output `% Off RRP with Uplift`; requires `margin` + `im_percent`.
- `docs/hpe_bid_xlsx.md` — HPE Bid (XLSX). HPE deal-export workbooks; header anchor `"LineType"` (one word); `Part Number`/`Bundle`/`BundleDetails` rows; vpn from `ProductNumber`/`BundleID`/`ComponentID` (OptionCode ignored); msrp from `ListPrcEst`, cost from `Offering`, qty from `Quantity` (not Min Order Qty), comments `"Max Qty: {MaxDealQty}"`; BundleDetails msrp/cost → `0.0001` sentinel; no quoted total; available templates `No Calculation` and `Uplift`.
- `docs/lenovo_brda_dcg_xlsx.md` — Lenovo BRDA DCG (XLS). Legacy `.xls` (OLE Compound Document) read via ExcelDataReader; PARENT/CHILD classified by whether the unit-price cell is `> 0` (explicit `0.0` is a child — `5374CM1` "Configuration Instruction" rows depend on this). LineSequence integer for parents (`"1"`, `"2"`), `parent.NN` for children (`"1.01"`). Quoted total from the `Total:` row's unit-price column, rounded 2 dp.
- `docs/lenovo_brda_dcg_xlsx.md` — Lenovo BRDA DCG (XLS). Legacy `.xls` (OLE Compound Document) read via ExcelDataReader; PARENT/CHILD classified by whether the unit-price cell is `> 0` (explicit `0.0` is a child — `5374CM1` "Configuration Instruction" rows depend on this). LineSequence is a single flat running sequence across parents and children (`"1"`, `"2"`, `"3"`, …); parent vs child is distinguished by cost, not by a dotted sequence. Quoted total from the `Total:` row's unit-price column, rounded 2 dp.
- `docs/lenovo_brda_dcg_pdf.md` — Lenovo BRDA DCG (PDF). Two variants: complex (PRODUCT AND SERVICE DETAILS + CONFIGURATION DETAILS — CONFIGs/PARENTs/children, detect 0.85) and simple (PRODUCT AND SERVICE DETAILS only — numbered parent items with real unit prices, no children, detect 0.75). Quirks: X1+1-based column boundaries for Part Number/Description; floating-point 1 pt offset for No/Qty in section 2; pre-description buffering; section 2 absent → empty children map.
- `docs/zebra_price_concession.md` — Zebra Price Concession (PDF + XLS). HTML-disguised XLS (HtmlAgilityPack); PDF uses anchor-based column detection with blank-word filtering and fused-number splitting (see memory/zebra-parser-pdfpig-quirks.md). Field map: Part No.→vpn, Max. Qty→qty, Min. Qty→min_qty, List Price→msrp (col H), Unit Special Price→cost (col I). Cancelled=Y rows: blank col H/I/W, qty=1, Comments="Cancelled (Standard Price)", warning modal. On Cost % written to col Z when provided. Available templates: No Calculation and Uplift.
- `docs/output_mapping.md` — how parsed `LineItem` fields map into `ANZ-GENERIC_ForeignUplift.xlsx`, output filename convention, locked output rules (MSRP column H stays empty; `serial_number` → Comments not Serial Number; term written only when `>= 1`). Read before generating any `*_parsed.xlsx`.
Expand Down
Binary file modified samples/outputs/55648855_parsed.xlsx
Binary file not shown.
Binary file modified samples/outputs/BRDAD010458440_NoCalculation_parsed.xlsx
Binary file not shown.
Binary file modified samples/outputs/BRDAD010458440_Uplift_parsed.xlsx
Binary file not shown.
Binary file modified samples/outputs/BRDAS010260417V1_parsed.xlsx
Binary file not shown.
Binary file not shown.
Binary file modified samples/outputs/Deals20260518T034809_HPI_Uplift_parsed.xlsx
Binary file not shown.
Binary file not shown.
Binary file modified samples/outputs/Deals20260518T043243_HPI_Uplift_parsed.xlsx
Binary file not shown.
Binary file modified samples/outputs/HPE_Deal_1601962887_v2_NoCalculation_parsed.xlsx
Binary file not shown.
Binary file modified samples/outputs/HPE_Deal_1601962887_v2_Uplift_parsed.xlsx
Binary file not shown.
17 changes: 5 additions & 12 deletions src/BidParser.Parsing/Hp/BidXlsx/HpBidXlsxParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ public ParseResult Parse(string path)
var items = new List<LineItem>();
var lastRow = sheet.LastRowUsed()?.RowNumber() ?? headerMap.RowNumber;

// Every emitted line — Part Number, Bundle, and Bundle Detail alike — takes the next
// number in a single running sequence: 1, 2, 3, … Bundle Detail rows are no longer
// sub-numbered under their parent bundle.
var lineCounter = 0;
var bundleParentSeq = 0;
var bundleChildCounter = 0;

for (var row = headerMap.RowNumber + 1; row <= lastRow; row++)
{
Expand Down Expand Up @@ -92,19 +93,11 @@ public ParseResult Parse(string path)
{
comments = $"Max Qty: {maxDealQty}";
}
if (lineType == "Bundle")
{
// A Bundle opens a child group: subsequent Bundle Detail rows
// sub-sequence under it (4.01, 4.02, …). A plain Part Number is
// never a parent, so it leaves the child counter untouched.
bundleParentSeq = lineCounter;
bundleChildCounter = 0;
}
break;

case "Bundle Detail":
bundleChildCounter++;
lineSequence = $"{bundleParentSeq}.{bundleChildCounter:D2}";
lineCounter++;
lineSequence = lineCounter.ToString();
qty = Int(sheet, row, headerMap, "Bundle Detail Qty");
rawMinQty = qty;
// A Bundle Detail is a component of its Bundle; the Bundle line carries
Expand Down
12 changes: 8 additions & 4 deletions src/BidParser.Parsing/Hp/OneConfigXlsx/HpOneConfigXlsxParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,15 @@ public ParseResult Parse(string path)

var items = new List<LineItem>();

// Every emitted line takes the next number in a single running sequence: the Config
// (parent) row is 1, and its components follow as 2, 3, 4, … rather than being
// sub-numbered under the parent (1.01, 1.02, …).
var lineCounter = 1;

// Parent (Config) row
items.Add(new LineItem
{
LineSequence = "1",
LineSequence = lineCounter.ToString(),
Vpn = configId,
Description = configName,
Qty = 1,
Expand All @@ -90,7 +95,6 @@ public ParseResult Parse(string path)
});

// Child (component) rows
var childCounter = 0;
var lastRow = sheet.LastRowUsed()?.RowNumber() ?? componentsHeaderRow;
for (var row = componentsHeaderRow + 1; row <= lastRow; row++)
{
Expand All @@ -105,14 +109,14 @@ public ParseResult Parse(string path)
continue;
}

childCounter++;
lineCounter++;
var description = WorkbookReader.CellText(sheet.Cell(row, componentsHeaderMap.Require("Description")));
var qtyText = WorkbookReader.CellText(sheet.Cell(row, componentsHeaderMap.Require("Quantity")));
var qty = DecimalCleaner.ParseOptionalInt(qtyText) ?? 1;

items.Add(new LineItem
{
LineSequence = $"1.{childCounter:D2}",
LineSequence = lineCounter.ToString(),
Vpn = partNumber,
Description = description,
Qty = qty,
Expand Down
16 changes: 5 additions & 11 deletions src/BidParser.Parsing/Hpe/BidXlsx/HpeBidXlsxParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ public ParseResult Parse(string path)
var items = new List<LineItem>();
var lastRow = sheet.LastRowUsed()?.RowNumber() ?? headerMap.RowNumber;

// Every emitted line (Bundle, Part Number, and BundleDetails alike) takes the next
// number in a single running sequence: 1, 2, 3, … to the end. BundleDetails rows are
// no longer sub-numbered under their parent bundle.
var lineCounter = 0;
var bundleParentSeq = 0;
var bundleChildCounter = 0;

for (var row = headerMap.RowNumber + 1; row <= lastRow; row++)
{
Expand Down Expand Up @@ -84,18 +85,11 @@ public ParseResult Parse(string path)
{
comments = $"Max Qty: {maxDealQty}";
}
if (lineType == "Bundle")
{
// A Bundle opens a child group: subsequent BundleDetails rows
// sub-sequence under it (1.01, 1.02, …).
bundleParentSeq = lineCounter;
bundleChildCounter = 0;
}
break;

case "BundleDetails":
bundleChildCounter++;
lineSequence = $"{bundleParentSeq}.{bundleChildCounter:D2}";
lineCounter++;
lineSequence = lineCounter.ToString();
vpn = Text(sheet, row, headerMap, "ComponentID");
// A BundleDetails line is a component of its Bundle; the Bundle line
// carries the total price, so the component's own msrp/cost are dropped to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,18 +385,21 @@ private static PdfWord FindSection2Header(IReadOnlyList<PdfWord> words, int star
private static List<LineItem> Assemble(List<Section1Entry> entries, Dictionary<int, List<ChildItem>> children)
{
var items = new List<LineItem>();
var globalSeq = 0;
// Every emitted line — parent config and child component alike — takes the next number
// in a single running sequence: 1, 2, 3, … Children are no longer sub-numbered under
// their parent (parent.NN).
var lineSeq = 0;

foreach (var entry in entries)
{
globalSeq++;
lineSeq++;
items.Add(new LineItem
{
Vpn = entry.Vpn,
Description = entry.Description,
Qty = entry.Qty,
Cost = entry.Cost,
LineSequence = globalSeq.ToString(),
LineSequence = lineSeq.ToString(),
Term = null,
Msrp = null,
MinQty = null,
Expand All @@ -405,25 +408,24 @@ private static List<LineItem> Assemble(List<Section1Entry> entries, Dictionary<i

if (entry.Type == Section1EntryType.Parent && children.TryGetValue(entry.LineNo, out var childList))
{
var childIdx = 0;
foreach (var child in childList)
{
// Skip the redundant "self" component: when a child's VPN matches its parent's VPN,
// the row is the parent unit re-listed inside its own component breakdown. Including
// it produces duplicate lines (e.g. 2 and 2.01 with the same VPN).
// it produces duplicate lines with the same VPN.
if (string.Equals(child.Vpn, entry.Vpn, StringComparison.OrdinalIgnoreCase))
{
continue;
}

childIdx++;
lineSeq++;
items.Add(new LineItem
{
Vpn = child.Vpn,
Description = child.Description,
Qty = child.Qty,
Cost = 0m,
LineSequence = $"{globalSeq}.{childIdx:D2}",
LineSequence = lineSeq.ToString(),
Term = null,
Msrp = null,
MinQty = null,
Expand Down
Loading
Loading