Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4303326
Merge pull request #1181 from waritctd/chore/forward-port-main-2026-1…
waritctd Oct 28, 2026
e3db815
fix(sales): the UI-walk P0s — VAT-inclusive deposit receipt, commissi…
waritctd Oct 28, 2026
e4d6564
fix(sales): PR #1182 review — cancel other open direct quotations at …
waritctd Oct 28, 2026
55706fd
test(e2e): commission invoice form defaults to the VAT-inclusive ยอดร…
waritctd Oct 28, 2026
e5c3efd
Merge pull request #1182 from waritctd/fix/sales-p0-ui-walk-2026-10-28
waritctd Oct 28, 2026
cdac04a
fix(sales): direct confirm-order sends once on a double-click; in-Tha…
waritctd Oct 28, 2026
0867c7a
Merge pull request #1185 from waritctd/fix/direct-confirm-double-clic…
waritctd Oct 28, 2026
76a216f
feat(sales): main deal-page button at ขั้น 10 (deposit notice) and ขั…
waritctd Oct 28, 2026
f9b0ceb
refactor: inline the one-use pickedRemainingInvoice temp in TicketDet…
waritctd Oct 28, 2026
781c1d7
fix(sales): ขั้น 10 button re-reads the notice list before creating; …
waritctd Oct 28, 2026
7314e57
Merge pull request #1184 from waritctd/claude/qa08-qa10-stage-cta-3052b0
waritctd Oct 28, 2026
00dc1a9
fix(dashboard): payment_overdue uses finance's overdue rule (VAT-incl…
waritctd Oct 28, 2026
d8a28f6
Merge pull request #1183 from waritctd/fix/dashboard-payable-matches-…
waritctd Oct 28, 2026
ce5894d
fix(security): stop serving customer data from the public site
waritctd Oct 28, 2026
2d03a08
Merge pull request #1187 from waritctd/fix/remove-public-customer-pii…
waritctd Oct 28, 2026
f058b9d
Merge remote-tracking branch 'origin/main' into release/2026-10-29-de…
waritctd Oct 29, 2026
e74fc9c
chore(release): render.yaml -> backend image v2026-10-29
waritctd Oct 29, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,8 @@ public class CommissionCalculator {
* here) and รับเงินเกิน (overpayment received, added back). Both are applied at the same
* "actualReceived" stage as the existing five deduction columns.
*
* <p><b>Owner confirmation needed:</b> the exact Excel formula for how these two columns
* combine with the others could not be extracted — LibreOffice dropped the cell formulas on
* conversion, leaving only static values behind. This sign convention (WHT subtracted,
* overpayment added, both at this stage, before the VAT strip) is a best-effort mirror of the
* columns' evident intent, not a verified transcription of the source formula. The
* sales-manager review step ({@link CommissionService#updateDeductions}) is the safety net for
* this slice — flag this for the owner to confirm against the Excel/accounting team before
* relying on it unreviewed.
* <p>Matches the owner's commission workbooks (L = D-E-F-G-H-I-J+K, the amount received incl. VAT;
* M = L/1.07).
*
* <p>Backward compatible: when {@code withholdingTax} and {@code overpayment} are both zero
* (or null), the result is identical to the pre-Slice-A1 five-column formula.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public CommissionRecord createFromDeal(
if (commissions.hasActiveCommissionForTicket(ticketId)) {
throw new ApiException(HttpStatus.CONFLICT, "มีรายการค่าคอมมิชชั่นสำหรับดีลนี้อยู่แล้ว");
}
BigDecimal effectiveGrossAmount = grossAmount != null ? grossAmount : tickets.payableAmountExVat(ticketId);
BigDecimal effectiveGrossAmount = grossAmount != null ? grossAmount : tickets.payableAmount(ticketId);

SubmitCommissionRequest request = new SubmitCommissionRequest(
ticketId,
Expand Down Expand Up @@ -386,7 +386,7 @@ private DealLinkage resolveDealLinkage(SubmitCommissionRequest request) {
throw new ApiException(HttpStatus.UNPROCESSABLE_CONTENT,
"ดีลนี้ยังไม่ถึงขั้นตอนรับชำระเงินครบถ้วน (CLOSED_PAID) จึงยังยื่นค่าคอมมิชชั่นไม่ได้");
}
BigDecimal payable = tickets.payableAmountExVat(ticketId);
BigDecimal payable = tickets.payableAmount(ticketId);
boolean mismatch = isMismatch(request.grossAmount(), payable);
return new DealLinkage(payable, mismatch);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import th.co.glr.hr.employee.ManagerApproverRepository;
import th.co.glr.hr.ticket.TicketRepository;

@Repository
public class DashboardRepository {
Expand Down Expand Up @@ -134,57 +135,7 @@
TicketSummaryDto summary = jdbc.queryForObject("""
WITH visible_tickets AS (
SELECT t.*
""" + from + where + """

Check warning on line 138 in backend/src/main/java/th/co/glr/hr/dashboard/DashboardRepository.java

View workflow job for this annotation

GitHub Actions / e2e-real

trailing white space will be removed

Check warning on line 138 in backend/src/main/java/th/co/glr/hr/dashboard/DashboardRepository.java

View workflow job for this annotation

GitHub Actions / build-and-test

trailing white space will be removed
), enriched_tickets AS (
SELECT vt.*,
COALESCE(accepted.total_amount, issued.total_amount, notice.total_payable,
item_total.total_amount, 0) AS payable_amount,
COALESCE(paid.total_paid, 0) AS paid_amount
FROM visible_tickets vt
LEFT JOIN LATERAL (
SELECT q.total_amount
FROM sales.quotation q
WHERE q.ticket_id = vt.ticket_id AND q.doc_status = 'ACCEPTED'
ORDER BY CASE q.recipient_type
WHEN 'BUYER' THEN 0
WHEN 'OWNER' THEN 1
ELSE 2
END,
q.accepted_at DESC NULLS LAST,
q.issued_at DESC,
q.quotation_id DESC
LIMIT 1
) accepted ON TRUE
LEFT JOIN LATERAL (
SELECT q.total_amount
FROM sales.quotation q
WHERE q.ticket_id = vt.ticket_id AND q.doc_status IN ('ISSUED','SENT')
ORDER BY CASE q.recipient_type
WHEN 'BUYER' THEN 0
WHEN 'OWNER' THEN 1
ELSE 2
END,
q.issued_at DESC,
q.quotation_id DESC
LIMIT 1
) issued ON TRUE
LEFT JOIN LATERAL (
SELECT d.total_payable
FROM sales.deposit_notice d
WHERE d.ticket_id = vt.ticket_id AND d.status = 'ISSUED'
ORDER BY d.version DESC, d.deposit_notice_id DESC
LIMIT 1
) notice ON TRUE
LEFT JOIN LATERAL (
SELECT COALESCE(SUM(COALESCE(ti.approved_price, 0) * ti.qty), 0) AS total_amount
FROM sales.ticket_item ti
WHERE ti.ticket_id = vt.ticket_id
) item_total ON TRUE
LEFT JOIN LATERAL (
SELECT COALESCE(SUM(CASE WHEN pr.kind = 'ADJUSTMENT' THEN -pr.amount ELSE pr.amount END), 0) AS total_paid
FROM sales.payment_receipt pr
WHERE pr.ticket_id = vt.ticket_id
) paid ON TRUE
)
SELECT COUNT(*) FILTER (WHERE status = 'draft') AS draft,
COUNT(*) FILTER (WHERE status = 'submitted') AS submitted,
Expand All @@ -205,12 +156,10 @@
) AS overdue_over_3days,
COUNT(*) FILTER (WHERE lifecycle = 'ON_HOLD') AS on_hold,
COUNT(*) FILTER (WHERE lifecycle = 'DORMANT') AS dormant,
COUNT(*) FILTER (
WHERE due_date < CURRENT_DATE
AND GREATEST(payable_amount - paid_amount, 0) > 0
COUNT(*) FILTER (WHERE """ + TicketRepository.paymentOverduePredicate("vt.ticket_id") + """
) AS payment_overdue,
COUNT(*) FILTER (WHERE fulfillment_status = 'PARTIALLY_DELIVERED') AS partially_delivered
FROM enriched_tickets
FROM visible_tickets vt
""",
params,
(rs, rowNum) -> new TicketSummaryDto(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,11 +835,16 @@ public record DealQuotationItemDto(
* Non-null ONLY on a TILE row of a document whose {@code priceMode} is {@code MIXED}; on
* every other row it is null and the document's mode applies
* ({@code WastageCalculator#rowPriceMode} is the one rule). */
String priceMode
String priceMode,
/** V194 — "IN_THAILAND" | "IN_TRANSIT" when this row came from a stock-flagged pricing-request
* line (owner ruling 2026-10-28: stock lines skip import, so they carry no import lead time);
* null = สั่งนำเข้า, and null on every DEAL_DIRECT row. Read-only: set at insert from the
* pricing-request item, never accepted from the client. */
String stockSource
) {
/** Ruling 3 (2026-09-30): the account view -- net unit price and line amount stay; every list-price / discount / CEO pricing field is emptied. */
public DealQuotationItemDto withoutPriceInternals() {
return new DealQuotationItemDto(id, seq, locationLabel, null, productCode, brand, model, color, texture, sizeText, thicknessMm, sqmPerPiece, quantityMode, areaSqm, piecesInput, wastageMode, wastageValue, piecesPerBox, null, null, originCountry, leadTimeMinDays, leadTimeMaxDays, itemNotes, piecesPerSqm, piecesBeforeWastage, piecesAfterWastage, piecesFinal, boxes, netUnitPrice, lineAmount, descriptionLine, sizeLine, null, lineType, quantity, unit, null, null, adjustmentDeadline, null, null, hasPicture, picturePlacement, pictureUrl, sqmPerBox, roundToFullBox, false, null, null, null, null, null, null, directNetPriceSqm, null);
return new DealQuotationItemDto(id, seq, locationLabel, null, productCode, brand, model, color, texture, sizeText, thicknessMm, sqmPerPiece, quantityMode, areaSqm, piecesInput, wastageMode, wastageValue, piecesPerBox, null, null, originCountry, leadTimeMinDays, leadTimeMaxDays, itemNotes, piecesPerSqm, piecesBeforeWastage, piecesAfterWastage, piecesFinal, boxes, netUnitPrice, lineAmount, descriptionLine, sizeLine, null, lineType, quantity, unit, null, null, adjustmentDeadline, null, null, hasPicture, picturePlacement, pictureUrl, sqmPerBox, roundToFullBox, false, null, null, null, null, null, null, directNetPriceSqm, null, stockSource);
}

/** This item carrying {@code value} as its own {@link #priceMode} — the same appended-field
Expand All @@ -854,7 +859,49 @@ public DealQuotationItemDto withPriceMode(String value) {
unit, specialPriceSqm, adjustmentPct, adjustmentDeadline, specialPriceLine, adjustmentAmount,
hasPicture, picturePlacement, pictureUrl, sqmPerBox, roundToFullBox,
priceChangedFromCeo, ceoListUnitPrice, ceoDiscountPct, ceoSpecialPriceSqm, ceoDirectNetPrice,
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, value);
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, value, stockSource);
}

/** The pre-stockSource canonical shape (58 components) -- stockSource null (import). */
public DealQuotationItemDto(
long id, int seq, String locationLabel, Long catalogPriceId, String productCode,
String brand, String model, String color, String texture, String sizeText,
BigDecimal thicknessMm, BigDecimal sqmPerPiece, String quantityMode, BigDecimal areaSqm,
Integer piecesInput, String wastageMode, BigDecimal wastageValue, Integer piecesPerBox,
BigDecimal unitPrice, BigDecimal discountPct, String originCountry,
Integer leadTimeMinDays, Integer leadTimeMaxDays, String itemNotes,
BigDecimal piecesPerSqm, int piecesBeforeWastage, int piecesAfterWastage, int piecesFinal,
Integer boxes, BigDecimal netUnitPrice, BigDecimal lineAmount,
String descriptionLine, String sizeLine, String calculationLine,
String lineType, BigDecimal quantity, String unit, BigDecimal specialPriceSqm,
BigDecimal adjustmentPct, LocalDate adjustmentDeadline, String specialPriceLine,
BigDecimal adjustmentAmount, boolean hasPicture, String picturePlacement, String pictureUrl,
BigDecimal sqmPerBox, boolean roundToFullBox, boolean priceChangedFromCeo,
BigDecimal ceoListUnitPrice, BigDecimal ceoDiscountPct, BigDecimal ceoSpecialPriceSqm,
BigDecimal ceoDirectNetPrice, BigDecimal ceoNetUnitPrice, BigDecimal listPriceSqm,
BigDecimal directNetPriceSqm, String priceMode) {
this(id, seq, locationLabel, catalogPriceId, productCode, brand, model, color, texture, sizeText,
thicknessMm, sqmPerPiece, quantityMode, areaSqm, piecesInput, wastageMode, wastageValue,
piecesPerBox, unitPrice, discountPct, originCountry, leadTimeMinDays, leadTimeMaxDays, itemNotes,
piecesPerSqm, piecesBeforeWastage, piecesAfterWastage, piecesFinal, boxes, netUnitPrice,
lineAmount, descriptionLine, sizeLine, calculationLine, lineType, quantity, unit,
specialPriceSqm, adjustmentPct, adjustmentDeadline, specialPriceLine, adjustmentAmount,
hasPicture, picturePlacement, pictureUrl, sqmPerBox, roundToFullBox, priceChangedFromCeo,
ceoListUnitPrice, ceoDiscountPct, ceoSpecialPriceSqm, ceoDirectNetPrice, ceoNetUnitPrice,
listPriceSqm, directNetPriceSqm, priceMode, null);
}

/** This item carrying {@code value} as its {@link #stockSource}. */
public DealQuotationItemDto withStockSource(String value) {
return new DealQuotationItemDto(id, seq, locationLabel, catalogPriceId, productCode, brand, model,
color, texture, sizeText, thicknessMm, sqmPerPiece, quantityMode, areaSqm, piecesInput,
wastageMode, wastageValue, piecesPerBox, unitPrice, discountPct, originCountry, leadTimeMinDays,
leadTimeMaxDays, itemNotes, piecesPerSqm, piecesBeforeWastage, piecesAfterWastage, piecesFinal,
boxes, netUnitPrice, lineAmount, descriptionLine, sizeLine, calculationLine, lineType, quantity,
unit, specialPriceSqm, adjustmentPct, adjustmentDeadline, specialPriceLine, adjustmentAmount,
hasPicture, picturePlacement, pictureUrl, sqmPerBox, roundToFullBox,
priceChangedFromCeo, ceoListUnitPrice, ceoDiscountPct, ceoSpecialPriceSqm, ceoDirectNetPrice,
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, priceMode, value);
}

/** The pre-GLA-123 canonical shape (no CEO-original/changed-flag fields) — kept so every
Expand Down Expand Up @@ -943,7 +990,7 @@ public DealQuotationItemDto withSqmPerBox(BigDecimal value) {
unit, specialPriceSqm, adjustmentPct, adjustmentDeadline, specialPriceLine, adjustmentAmount,
hasPicture, picturePlacement, pictureUrl, value, roundToFullBox,
priceChangedFromCeo, ceoListUnitPrice, ceoDiscountPct, ceoSpecialPriceSqm, ceoDirectNetPrice,
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, priceMode);
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, priceMode, stockSource);
}

/** This item carrying {@code value} as its {@link #roundToFullBox} — same device as
Expand All @@ -958,7 +1005,7 @@ public DealQuotationItemDto withRoundToFullBox(boolean value) {
unit, specialPriceSqm, adjustmentPct, adjustmentDeadline, specialPriceLine, adjustmentAmount,
hasPicture, picturePlacement, pictureUrl, sqmPerBox, value,
priceChangedFromCeo, ceoListUnitPrice, ceoDiscountPct, ceoSpecialPriceSqm, ceoDirectNetPrice,
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, priceMode);
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, priceMode, stockSource);
}

/** This item carrying the GLA-123 CEO-comparison fields — same device as
Expand All @@ -974,7 +1021,7 @@ public DealQuotationItemDto withCeoComparison(boolean changed, BigDecimal listUn
unit, specialPriceSqm, adjustmentPct, adjustmentDeadline, specialPriceLine, adjustmentAmount,
hasPicture, picturePlacement, pictureUrl, sqmPerBox, roundToFullBox,
changed, listUnitPrice, discountPct2, specialPriceSqm2, directNetPrice, netUnitPrice2,
listPriceSqm, directNetPriceSqm, priceMode);
listPriceSqm, directNetPriceSqm, priceMode, stockSource);
}

/** This item carrying the per-ตร.ม. prices as the rep typed them (V192.2) — the same
Expand All @@ -990,7 +1037,7 @@ public DealQuotationItemDto withPerSqmPrices(BigDecimal list, BigDecimal directN
unit, specialPriceSqm, adjustmentPct, adjustmentDeadline, specialPriceLine, adjustmentAmount,
hasPicture, picturePlacement, pictureUrl, sqmPerBox, roundToFullBox,
priceChangedFromCeo, ceoListUnitPrice, ceoDiscountPct, ceoSpecialPriceSqm, ceoDirectNetPrice,
ceoNetUnitPrice, list, directNet, priceMode);
ceoNetUnitPrice, list, directNet, priceMode, stockSource);
}

/** This item with its picture fields set from the stored link (repository read path). */
Expand All @@ -1006,7 +1053,7 @@ public DealQuotationItemDto withPicture(long quotationId, String placement) {
has ? "/api/deal-quotations/" + quotationId + "/items/" + id + "/picture" : null, sqmPerBox,
roundToFullBox,
priceChangedFromCeo, ceoListUnitPrice, ceoDiscountPct, ceoSpecialPriceSqm, ceoDirectNetPrice,
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, priceMode);
ceoNetUnitPrice, listPriceSqm, directNetPriceSqm, priceMode, stockSource);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,18 @@ public List<ExpiredQuotationRow> expireOverdueQuotations() {
rs.getLong("ticket_id"), rs.getString("number")));
}

/** Owner review 2026-10-28: confirming an order from {@code keepId} cancels the deal's OTHER open DEAL_DIRECT
* quotations (DRAFT / PENDING_APPROVAL), which could otherwise never be submitted or approved again. Returns the
* cancelled numbers so the caller can log them. PRICING_REQUEST-origin rows are never touched. */
public List<String> cancelOtherOpenDirectQuotations(long ticketId, long keepId) {
return jdbc.queryForList("""
UPDATE sales.quotation SET doc_status = 'CANCELLED', updated_at = now()
WHERE ticket_id = :ticketId AND quotation_id <> :keepId AND origin = 'DEAL_DIRECT'
AND doc_status IN ('DRAFT', 'PENDING_APPROVAL')
RETURNING number
""", Map.of("ticketId", ticketId, "keepId", keepId), String.class);
}

/** Compare-and-set DRAFT -> CANCELLED. */
public int cancel(long quotationId) {
return jdbc.update("""
Expand Down Expand Up @@ -1827,7 +1839,7 @@ private List<DealQuotationItemDto> findItems(long quotationId) {
final_unit_price, amount,
raw_unit, description, line_type, qi.special_price_sqm, adjustment_pct, adjustment_deadline,
picture_placement, sqm_per_box, round_to_full_box,
qi.list_price_sqm, qi.direct_net_price_sqm,
qi.list_price_sqm, qi.direct_net_price_sqm, qi.stock_source,
-- V192.5: the ROW's own mode. Aliased because "price_mode" below is already the
-- DOCUMENT's (the subselect), and #mapItemColumns needs both to tell them apart.
qi.price_mode AS item_price_mode,
Expand Down Expand Up @@ -1878,7 +1890,7 @@ private Map<Long, List<DealQuotationItemDto>> findItemsForQuotations(List<Long>
final_unit_price, amount,
raw_unit, description, line_type, qi.special_price_sqm, adjustment_pct, adjustment_deadline,
picture_placement, sqm_per_box, round_to_full_box,
qi.list_price_sqm, qi.direct_net_price_sqm,
qi.list_price_sqm, qi.direct_net_price_sqm, qi.stock_source,
-- V192.5: the ROW's own mode. Aliased because "price_mode" below is already the
-- DOCUMENT's (the subselect), and #mapItemColumns needs both to tell them apart.
qi.price_mode AS item_price_mode,
Expand Down Expand Up @@ -2254,7 +2266,7 @@ private boolean ceoPriceModeChanged(ResultSet rs) throws SQLException {
* the bytes are served separately by {@link #findItemPicture}. An item without a picture
* keeps the legacy constructor's {@code hasPicture=false} shape exactly. */
private DealQuotationItemDto mapItem(ResultSet rs, Map<Long, CatalogSqmBasis> basisByPriceId) throws SQLException {
DealQuotationItemDto item = mapItemColumns(rs, basisByPriceId);
DealQuotationItemDto item = mapItemColumns(rs, basisByPriceId).withStockSource(rs.getString("stock_source"));
String placement = rs.getString("picture_placement");
return placement == null ? item : item.withPicture(rs.getLong("quotation_id"), placement);
}
Expand Down
Loading
Loading