From eb39f42b8c5b371145ece14c2b48cd9a497dcbe8 Mon Sep 17 00:00:00 2001 From: ahasani194 Date: Sat, 6 Jun 2026 19:52:58 +0330 Subject: [PATCH] feat: add correction and return from sale functionalities to sale invoices - Added new API routes for correction and return from sale in POS_SALE_INVOICES_API_ROUTES. - Implemented correction and return from sale methods in PosSaleInvoicesService. - Updated PosSaleInvoiceStore to include actions for correction and return from sale. - Enhanced single.component to handle correction and return from sale events. - Created correction and return from sale forms with appropriate validation and submission logic. - Updated sale-invoice-single-view component to manage correction and return from sale actions. - Added models for correction and return from sale requests. - Improved user interface messages and form handling for better user experience. --- .../saleInvoices/constants/apiRoutes/index.ts | 2 + .../modules/saleInvoices/models/correction.ts | 10 ++ .../saleInvoices/models/returnFromSale.ts | 10 ++ .../saleInvoices/services/main.service.ts | 28 +++- .../modules/saleInvoices/store/main.store.ts | 8 + .../saleInvoices/views/single.component.html | 2 + .../saleInvoices/views/single.component.ts | 10 +- .../components/views/list-view.component.ts | 3 +- .../pos/modules/shop/store/main.store.ts | 2 + .../components/input/input.component.ts | 17 +- .../correctionForm/form.component.html | 28 ++++ .../invoices/correctionForm/form.component.ts | 152 ++++++++++++++++++ .../invoices/correctionForm/index.ts | 1 + .../invoices/models/correction.d.ts | 21 +++ .../components/invoices/models/index.ts | 2 + .../components/invoices/models/return.d.ts | 7 + .../invoices/returnForm/form.component.html | 9 +- .../invoices/returnForm/form.component.ts | 31 +++- .../sale-invoice-full-response.model.ts | 1 + .../sale-invoice-single-view.component.html | 13 +- .../sale-invoice-single-view.component.ts | 93 ++++++++++- .../posPayment/gold-form.component.html | 4 +- .../posPayment/gold-form.component.ts | 27 +++- .../posPayment/standard-form.component.html | 4 +- .../posPayment/standard-form.component.ts | 27 +++- .../datepicker/datepicker.component.html | 2 +- 26 files changed, 472 insertions(+), 42 deletions(-) create mode 100644 src/app/domains/pos/modules/saleInvoices/models/correction.ts create mode 100644 src/app/domains/pos/modules/saleInvoices/models/returnFromSale.ts create mode 100644 src/app/shared/components/invoices/correctionForm/form.component.html create mode 100644 src/app/shared/components/invoices/correctionForm/form.component.ts create mode 100644 src/app/shared/components/invoices/correctionForm/index.ts create mode 100644 src/app/shared/components/invoices/models/correction.d.ts create mode 100644 src/app/shared/components/invoices/models/index.ts create mode 100644 src/app/shared/components/invoices/models/return.d.ts diff --git a/src/app/domains/pos/modules/saleInvoices/constants/apiRoutes/index.ts b/src/app/domains/pos/modules/saleInvoices/constants/apiRoutes/index.ts index 3b0be7c..92a6e4e 100644 --- a/src/app/domains/pos/modules/saleInvoices/constants/apiRoutes/index.ts +++ b/src/app/domains/pos/modules/saleInvoices/constants/apiRoutes/index.ts @@ -6,6 +6,8 @@ export const POS_SALE_INVOICES_API_ROUTES = { tsp: { send: (invoiceId: string) => `${baseUrl()}/${invoiceId}/send`, getInquiry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/inquiry`, + correction: (invoiceId: string) => `${baseUrl()}/${invoiceId}/correction`, + returnFromSale: (invoiceId: string) => `${baseUrl()}/${invoiceId}/return_from_sale`, revoke: (invoiceId: string) => `${baseUrl()}/${invoiceId}/revoke`, retry: (invoiceId: string) => `${baseUrl()}/${invoiceId}/retry`, status: (invoiceId: string) => `${baseUrl()}/${invoiceId}/fiscal/status`, diff --git a/src/app/domains/pos/modules/saleInvoices/models/correction.ts b/src/app/domains/pos/modules/saleInvoices/models/correction.ts new file mode 100644 index 0000000..0e70634 --- /dev/null +++ b/src/app/domains/pos/modules/saleInvoices/models/correction.ts @@ -0,0 +1,10 @@ +import { IPosOrderItem } from '../../shop/models'; + +export interface IPosCorrectionRequest { + total_amount: number; + discount_amount: number; + tax_amount: number; + invoice_date: string; + items: IPosOrderItem[]; + notes?: string; +} diff --git a/src/app/domains/pos/modules/saleInvoices/models/returnFromSale.ts b/src/app/domains/pos/modules/saleInvoices/models/returnFromSale.ts new file mode 100644 index 0000000..3cff702 --- /dev/null +++ b/src/app/domains/pos/modules/saleInvoices/models/returnFromSale.ts @@ -0,0 +1,10 @@ +import { IPosOrderItem } from '../../shop/models'; + +export interface IPosReturnFromSaleRequest { + total_amount: number; + discount_amount: number; + tax_amount: number; + invoice_date: string; + items: IPosOrderItem[]; + notes?: string; +} diff --git a/src/app/domains/pos/modules/saleInvoices/services/main.service.ts b/src/app/domains/pos/modules/saleInvoices/services/main.service.ts index 00a6803..888b4ed 100644 --- a/src/app/domains/pos/modules/saleInvoices/services/main.service.ts +++ b/src/app/domains/pos/modules/saleInvoices/services/main.service.ts @@ -21,11 +21,11 @@ export class PosSaleInvoicesService { private apiRoutes = POS_SALE_INVOICES_API_ROUTES; getAll( - query: IPosSaleInvoicesFilterDto = {}, + query: IPosSaleInvoicesFilterDto = {} ): Observable> { return this.http.get>( this.apiRoutes.list(), - { params: query as Record }, + { params: query as Record } ); } @@ -36,14 +36,14 @@ export class PosSaleInvoicesService { sendToTsp(invoiceId: string): Observable { return this.http.post( this.apiRoutes.tsp.send(invoiceId), - {}, + {} ); } retrySendToTsp(invoiceId: string): Observable { return this.http.post( this.apiRoutes.tsp.retry(invoiceId), - {}, + {} ); } @@ -54,20 +54,34 @@ export class PosSaleInvoicesService { getInquiry(invoiceId: string): Observable { return this.http.get( this.apiRoutes.tsp.getInquiry(invoiceId), - {}, + {} + ); + } + + correction(data: any, invoiceId: string): Observable { + return this.http.post( + this.apiRoutes.tsp.correction(invoiceId), + data + ); + } + + returnFromSale(data: any, invoiceId: string): Observable { + return this.http.post( + this.apiRoutes.tsp.returnFromSale(invoiceId), + data ); } revoke(invoiceId: string): Observable { return this.http.post( this.apiRoutes.tsp.revoke(invoiceId), - {}, + {} ); } getFiscalAttempts(invoiceId: string): Observable { return this.http.get( - this.apiRoutes.tsp.attempts(invoiceId), + this.apiRoutes.tsp.attempts(invoiceId) ); } } diff --git a/src/app/domains/pos/modules/saleInvoices/store/main.store.ts b/src/app/domains/pos/modules/saleInvoices/store/main.store.ts index c96dc9f..9a79288 100644 --- a/src/app/domains/pos/modules/saleInvoices/store/main.store.ts +++ b/src/app/domains/pos/modules/saleInvoices/store/main.store.ts @@ -4,6 +4,8 @@ import { TspProviderResponseStatus } from '@/shared/catalog'; import taxProviderStatusTranslatorUtil from '@/shared/catalog/taxProviderStatus/tax-provider-status-translator.util'; import { inject, Injectable } from '@angular/core'; import { catchError, finalize } from 'rxjs'; +import { IPosCorrectionRequest } from '../models/correction'; +import { IPosReturnFromSaleRequest } from '../models/returnFromSale'; import { PosSaleInvoicesService } from '../services/main.service'; interface PosSaleInvoiceState extends EntityState {} @@ -53,6 +55,12 @@ export class PosSaleInvoiceStore extends EntityStore< sendToTsp(invoceId: string) { return this.service.sendToTsp(invoceId); } + correction(data: IPosCorrectionRequest, invoceId: string) { + return this.service.correction(data, invoceId); + } + returnFromSale(data: IPosReturnFromSaleRequest, invoceId: string) { + return this.service.returnFromSale(data, invoceId); + } inquiry(invoceId: string) { return this.service.getInquiry(invoceId); } diff --git a/src/app/domains/pos/modules/saleInvoices/views/single.component.html b/src/app/domains/pos/modules/saleInvoices/views/single.component.html index 762b573..383cd66 100644 --- a/src/app/domains/pos/modules/saleInvoices/views/single.component.html +++ b/src/app/domains/pos/modules/saleInvoices/views/single.component.html @@ -7,5 +7,7 @@ [inquiryLoading]="inquiryLoading()" (onSendToTsp)="sendToTsp()" (onInquiry)="inquiry()" + (onCorrection)="correction($event)" + (onReturnFromSale)="returnFromSale($event)" variant="pos" /> diff --git a/src/app/domains/pos/modules/saleInvoices/views/single.component.ts b/src/app/domains/pos/modules/saleInvoices/views/single.component.ts index 89cbfe9..f81c7ed 100644 --- a/src/app/domains/pos/modules/saleInvoices/views/single.component.ts +++ b/src/app/domains/pos/modules/saleInvoices/views/single.component.ts @@ -6,6 +6,8 @@ import { Component, computed, inject, signal } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { finalize } from 'rxjs'; import { posSaleInvoicesNamedRoutes } from '../constants'; +import { IPosCorrectionRequest } from '../models/correction'; +import { IPosReturnFromSaleRequest } from '../models/returnFromSale'; import { PosSaleInvoiceStore } from '../store/main.store'; @Component({ @@ -78,10 +80,10 @@ export class PosSaleInvoiceComponent { ) .subscribe(); } - correction() { + correction(data: IPosCorrectionRequest) { this.correctionLoading.set(true); this.store - .sendToTsp(this.invoiceId()) + .correction(data, this.invoiceId()) .pipe( finalize(() => { this.correctionLoading.set(false); @@ -89,10 +91,10 @@ export class PosSaleInvoiceComponent { ) .subscribe(); } - backFromSale() { + returnFromSale(data: IPosReturnFromSaleRequest) { this.backFromSaleLoading.set(true); this.store - .sendToTsp(this.invoiceId()) + .returnFromSale(data, this.invoiceId()) .pipe( finalize(() => { this.backFromSaleLoading.set(false); diff --git a/src/app/domains/pos/modules/shop/components/views/list-view.component.ts b/src/app/domains/pos/modules/shop/components/views/list-view.component.ts index 736d262..5eb016a 100644 --- a/src/app/domains/pos/modules/shop/components/views/list-view.component.ts +++ b/src/app/domains/pos/modules/shop/components/views/list-view.component.ts @@ -11,7 +11,6 @@ import { } from '@angular/core'; import { ButtonDirective } from 'primeng/button'; import { Skeleton } from 'primeng/skeleton'; -import { Tag } from 'primeng/tag'; import images from 'src/assets/images'; import { PosLandingStore } from '../../store/main.store'; import { FavoriteCTAComponent } from './favorite-CTA.component'; @@ -20,7 +19,7 @@ import { FavoriteCTAComponent } from './favorite-CTA.component'; selector: 'pos-goods-list-view', templateUrl: './list-view.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ButtonDirective, Skeleton, FavoriteCTAComponent, Tag], + imports: [ButtonDirective, Skeleton, FavoriteCTAComponent], }) export class PosGoodsListViewComponent { private readonly store = inject(PosLandingStore); diff --git a/src/app/domains/pos/modules/shop/store/main.store.ts b/src/app/domains/pos/modules/shop/store/main.store.ts index a97cebb..c50a63c 100644 --- a/src/app/domains/pos/modules/shop/store/main.store.ts +++ b/src/app/domains/pos/modules/shop/store/main.store.ts @@ -136,6 +136,7 @@ export class PosLandingStore { } private async getGoods() { + // if (this.state$().goods) return; this.setState({ getGoodsLoading: true }); try { const res = await firstValueFrom(this.service.getGoods(this.state$().searchQuery)); @@ -146,6 +147,7 @@ export class PosLandingStore { } private async getGoodCategories() { + // if (this.state$().goodCategories) return; this.setState({ getGoodCategoriesLoading: true }); try { const res = await firstValueFrom(this.service.getGoodCategories()); diff --git a/src/app/shared/components/input/input.component.ts b/src/app/shared/components/input/input.component.ts index de812c7..6bd8fad 100644 --- a/src/app/shared/components/input/input.component.ts +++ b/src/app/shared/components/input/input.component.ts @@ -243,9 +243,14 @@ export class InputComponent { } private isOutOfRange(numericValue: number): { min: boolean; max: boolean } { + console.log('min', this.min, 'max', this.max, 'numericValue', numericValue); + + const min = this.min !== undefined && this.min !== null ? this.min : Number.NEGATIVE_INFINITY; + const max = this.max !== undefined && this.max !== null ? this.max : Number.POSITIVE_INFINITY; + return { - min: !!((this.min || this.min === 0) && numericValue < this.min), - max: !!((this.max || this.max === 0) && numericValue > this.max), + min: !!((min || min === 0) && numericValue < min), + max: !!((max || max === 0) && numericValue > max), }; } @@ -272,9 +277,12 @@ export class InputComponent { const controlValue = isIdentifierField ? value : value === '' ? '' : numericValue; this.control.setValue(controlValue); target.value = value; + console.log('value', value, 'controlValue', controlValue, 'numericValue', numericValue); } onInput($event: Event) { + console.log('$event', $event); + const target = $event.target as HTMLInputElement | null; if (!target) return; @@ -288,11 +296,14 @@ export class InputComponent { value = this.normalizeLeadingZeros(value, allowDecimal); value = this.enforceMaxLength(value); - + console.log('$value', value); let numericValue = this.parseNumericValue(value); const range = this.isOutOfRange(numericValue); + console.log('$range', range); if (value !== '' && (range.min || range.max)) { + console.log('in if'); + this.notifyRangeError(range); value = this.lastValidNumericValue; numericValue = this.parseNumericValue(value); diff --git a/src/app/shared/components/invoices/correctionForm/form.component.html b/src/app/shared/components/invoices/correctionForm/form.component.html new file mode 100644 index 0000000..11f927e --- /dev/null +++ b/src/app/shared/components/invoices/correctionForm/form.component.html @@ -0,0 +1,28 @@ +
+ + + @for (item of initialValues || []; track item.id) { +
+ @if ($index) { +
+ } + + {{ $index + 1 }}- {{ item.good_snapshot.name }} + + @if (isGold(item)) { + + } @else { + + } +
+ } + + + diff --git a/src/app/shared/components/invoices/correctionForm/form.component.ts b/src/app/shared/components/invoices/correctionForm/form.component.ts new file mode 100644 index 0000000..5eb074b --- /dev/null +++ b/src/app/shared/components/invoices/correctionForm/form.component.ts @@ -0,0 +1,152 @@ +import ISummary from '@/core/models/summary'; +import { IPosOrderItem } from '@/domains/pos/modules/shop/models'; +import { AbstractForm } from '@/shared/abstractClasses'; +import { + SharedGoldPayloadFormComponent, + SharedStandardPayloadFormComponent, +} from '@/shared/components'; +import { goldDiscountType } from '@/shared/localEnum/constants/goldDiscountType'; +import { IGoldPayload, IStandardPayload } from '@/shared/models'; +import { formatDate, GREGORIAN_DATE_FORMATS } from '@/utils'; +import { Component, Input, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Divider } from 'primeng/divider'; +import { FieldInvoiceDateComponent } from '../../fields/invoice_date.component'; +import { FormFooterActionsComponent } from '../../formFooterActions/form-footer-actions.component'; +import { CorrectionInvoiceFormValue, TCorrectionItemPayload } from '../models'; +import { IInvoiceItem } from '../sale-invoice-full-response.model'; + +@Component({ + selector: 'shared-correction-form', + templateUrl: 'form.component.html', + imports: [ + ReactiveFormsModule, + FieldInvoiceDateComponent, + FormFooterActionsComponent, + SharedGoldPayloadFormComponent, + SharedStandardPayloadFormComponent, + Divider, + ], +}) +export class SharedCorrectionFormComponent extends AbstractForm< + CorrectionInvoiceFormValue, + CorrectionInvoiceFormValue, + IInvoiceItem[] +> { + @Input({ required: true }) invoiceDate!: string; + + @ViewChildren(SharedGoldPayloadFormComponent) + goldForms!: QueryList; + @ViewChildren(SharedStandardPayloadFormComponent) + standardForms!: QueryList; + + form = this.fb.group({ + invoice_date: [this.invoiceDate], + }); + + itemDrafts: Record = {}; + mappedInitialItems: TCorrectionItemPayload[] = []; + + initForm() { + this.form.controls.invoice_date.setValue( + formatDate(this.invoiceDate, 'gregory', 'en', GREGORIAN_DATE_FORMATS.FULL) + ); + this.itemDrafts = {}; + this.mappedInitialItems = (this.initialValues || []).map((item) => this.mapToInitialItem(item)); + } + + override ngOnInit(): void { + this.initForm(); + } + + override ngOnChanges(changes: SimpleChanges) { + if (changes['initialValues'] || changes['invoiceDate']) { + this.initForm(); + } + } + + isGold(item: IInvoiceItem) { + return String(item.good_snapshot?.pricing_model || '').toUpperCase() === 'GOLD'; + } + + vatFromItem(item: IInvoiceItem) { + return Number(item.good_snapshot?.sku?.VAT || 0); + } + + private mapBaseItem(item: IInvoiceItem): Omit { + return { + id: item.id, + pricing_model: item.good_snapshot?.pricing_model || '', + good_id: item.good_id, + unit_price: Number(item.unit_price || 0), + quantity: Number(item.quantity || 0), + discount_amount: Number(item.discount || 0), + total_amount: Number(item.total_amount || 0), + tax_amount: 0, + base_total_amount: Number(item.unit_price || 0) * Number(item.quantity || 0), + measure_unit: (item.good_snapshot?.measure_unit || {}) as ISummary, + }; + } + + mapToGoldInitialItem(item: IInvoiceItem): IPosOrderItem { + return { + ...this.mapBaseItem(item), + payload: { + karat: (item.payload as any)?.karat, + wages: Number((item.payload as any)?.wages || 0), + commission: Number((item.payload as any)?.commission || 0), + profit: Number((item.payload as any)?.profit || 0), + discount_type: goldDiscountType.PROFIT, + } as IGoldPayload, + }; + } + + mapToStandardInitialItem(item: IInvoiceItem): IPosOrderItem { + return { + ...this.mapBaseItem(item), + payload: {} as IStandardPayload, + }; + } + + mapToInitialItem(item: IInvoiceItem): TCorrectionItemPayload { + if (this.isGold(item)) { + return this.mapToGoldInitialItem(item) as TCorrectionItemPayload; + } + return this.mapToStandardInitialItem(item) as TCorrectionItemPayload; + } + + override submitForm() { + const goldQueue = [...(this.goldForms?.toArray() || [])]; + const standardQueue = [...(this.standardForms?.toArray() || [])]; + const items = + this.initialValues?.map((item, index) => { + const current = this.isGold(item) + ? goldQueue.shift()?.getCorrectionValue() + : standardQueue.shift()?.getCorrectionValue(); + const merged = (current || this.mappedInitialItems[index]) as TCorrectionItemPayload; + return { + ...merged, + id: item.id, + pricing_model: item.good_snapshot?.pricing_model || '', + } as TCorrectionItemPayload; + }) || []; + + console.log('items', items); + + const hasChanges = (this.initialValues || []).some((item, index) => { + const source = this.mappedInitialItems[index]; + const draft = items[index] || source; + return JSON.stringify(source) !== JSON.stringify(draft); + }); + + if (!hasChanges) { + this.toastService.warn({ text: 'هیچ تغییری در مقادیر ثبت نشده است.' }); + return; + } + + this.onSubmit.emit({ + invoice_date: this.form.controls.invoice_date.value || '', + items, + }); + } +} diff --git a/src/app/shared/components/invoices/correctionForm/index.ts b/src/app/shared/components/invoices/correctionForm/index.ts new file mode 100644 index 0000000..780c593 --- /dev/null +++ b/src/app/shared/components/invoices/correctionForm/index.ts @@ -0,0 +1 @@ +export * from './form.component'; diff --git a/src/app/shared/components/invoices/models/correction.d.ts b/src/app/shared/components/invoices/models/correction.d.ts new file mode 100644 index 0000000..116ec8c --- /dev/null +++ b/src/app/shared/components/invoices/models/correction.d.ts @@ -0,0 +1,21 @@ +import { IPosOrderItem } from '@/domains/pos/modules/shop/models'; +import { IGoldPayload, IStandardPayload } from '@/shared/models'; + +export type TCorrectionItemPayload = IPosOrderItem & { + id: string; + pricing_model: string; +}; + +export type CorrectionInvoiceFormValue = { + invoice_date: string; + items: TCorrectionItemPayload[]; +}; + +export interface ICorrectionRequest { + total_amount: number; + discount_amount: number; + tax_amount: number; + invoice_date: string; + items: IPosOrderItem[]; + notes?: string; +} diff --git a/src/app/shared/components/invoices/models/index.ts b/src/app/shared/components/invoices/models/index.ts new file mode 100644 index 0000000..4ecf770 --- /dev/null +++ b/src/app/shared/components/invoices/models/index.ts @@ -0,0 +1,2 @@ +export * from './correction'; +export * from './return'; diff --git a/src/app/shared/components/invoices/models/return.d.ts b/src/app/shared/components/invoices/models/return.d.ts new file mode 100644 index 0000000..56d0e62 --- /dev/null +++ b/src/app/shared/components/invoices/models/return.d.ts @@ -0,0 +1,7 @@ +export type ReturnFromSaleFormValue = { + items: { + id: string; + quantity: number; + maxQuantity: number; + }[]; +}; diff --git a/src/app/shared/components/invoices/returnForm/form.component.html b/src/app/shared/components/invoices/returnForm/form.component.html index d0b10ea..13096c1 100644 --- a/src/app/shared/components/invoices/returnForm/form.component.html +++ b/src/app/shared/components/invoices/returnForm/form.component.html @@ -1,7 +1,10 @@
- - در برگشت از فروش صورتحساب شما میتوانید صرفا تعداد محصولات و خدمات خود را کم و حذف کنید و حداقل مقدار محصولات و خدمات - شما ۱ می باشد. + +
    +
  1. تعداد محصولات/خدمات خود را کم و یا حذف کنید
  2. +
  3. حداقل ۱ خدمت/سرویس بایستی باقی بماند
  4. +
  5. تخفیف‌ها، مالیات و قیمت نهایی تمام شده با توجه به مقادیر فاکتور اصلی به صورت سیستمی محاسبه خواهند شد.
  6. +
diff --git a/src/app/shared/components/invoices/returnForm/form.component.ts b/src/app/shared/components/invoices/returnForm/form.component.ts index 42acb51..810ea4c 100644 --- a/src/app/shared/components/invoices/returnForm/form.component.ts +++ b/src/app/shared/components/invoices/returnForm/form.component.ts @@ -10,7 +10,9 @@ import { IInvoiceItem } from '../sale-invoice-full-response.model'; import { SharedReturnFormItemComponent } from './item-form.component'; type ItemForm = FormGroup<{ + id: FormControl; quantity: FormControl; + maxQuantity: FormControl; }>; type BackFromSaleFormValue = { @@ -71,12 +73,15 @@ export class SharedReturnFormComponent extends AbstractForm< ); this.initialValues?.forEach((item: any) => { + const maxQuantity = Number(item.quantity || 0); this.items.push( this.fb.group({ + id: [item.id || null], quantity: [ - Number(item.quantity), - [Validators.required, Validators.min(0), Validators.max(Number(item.quantity))], + maxQuantity, + [Validators.required, Validators.min(0), Validators.max(maxQuantity)], ], + maxQuantity: [maxQuantity], }) ); }); @@ -93,16 +98,32 @@ export class SharedReturnFormComponent extends AbstractForm< } override submitForm() { - const payload = this.items.controls.map((control) => ({ - id: control.get('id')?.value, - quantity: control.get('quantity')?.value, + const payload = this.items.getRawValue().map((item) => ({ + id: item.id || '', + quantity: Number(item.quantity || 0), + maxQuantity: Number(item.maxQuantity || 0), })); + const hasChanges = payload.some( + (item, index) => item.quantity !== Number(this.initialValues?.[index]?.quantity || 0) + ); + + if (!hasChanges) { + this.toastService.warn({ + text: 'هیچ تغییری در مقادیر ثبت نشده است.', + }); + return; + } + if (!payload.some((item) => item.quantity)) { this.toastService.warn({ text: 'حداقل مقدار یکی از کالاها/خدمات باید بیشتر از ۰ باشد.', }); return; } + + this.onSubmit.emit({ + items: payload.filter((item) => !!item.id), + }); } } diff --git a/src/app/shared/components/invoices/sale-invoice-full-response.model.ts b/src/app/shared/components/invoices/sale-invoice-full-response.model.ts index e0272f4..c85641b 100644 --- a/src/app/shared/components/invoices/sale-invoice-full-response.model.ts +++ b/src/app/shared/components/invoices/sale-invoice-full-response.model.ts @@ -24,6 +24,7 @@ export interface ISaleInvoiceFullRawResponse { main_id: Maybe; tax_id: Maybe; reference_invoice: Maybe; + refrence_by: Maybe; notes: Maybe; settlement_type: IEnumTranslate; } diff --git a/src/app/shared/components/invoices/sale-invoice-single-view.component.html b/src/app/shared/components/invoices/sale-invoice-single-view.component.html index 3612071..b6dcc1f 100644 --- a/src/app/shared/components/invoices/sale-invoice-single-view.component.html +++ b/src/app/shared/components/invoices/sale-invoice-single-view.component.html @@ -111,7 +111,16 @@ - - + + + + + } diff --git a/src/app/shared/components/invoices/sale-invoice-single-view.component.ts b/src/app/shared/components/invoices/sale-invoice-single-view.component.ts index 01cfc18..82f606c 100644 --- a/src/app/shared/components/invoices/sale-invoice-single-view.component.ts +++ b/src/app/shared/components/invoices/sale-invoice-single-view.component.ts @@ -2,6 +2,8 @@ import { Maybe } from '@/core'; import { NativeBridgeService } from '@/core/services'; import { ToastService } from '@/core/services/toast.service'; import { PosConfigPrintService } from '@/domains/pos/modules/configs/components/print/services/main.service'; +import { IPosReturnFromSaleRequest } from '@/domains/pos/modules/saleInvoices/models/returnFromSale'; +import { IPosOrderItem } from '@/domains/pos/modules/shop/models'; import { PosInfoStore } from '@/domains/pos/store'; import { CatalogInvoiceTypeTagComponent, @@ -44,6 +46,8 @@ import { ProgressSpinner } from 'primeng/progressspinner'; import { TableModule } from 'primeng/table'; import { Observable } from 'rxjs'; import { AppConfirmationService } from '../confirmationDialog/confirmation-dialog.service'; +import { SharedCorrectionFormComponent } from './correctionForm'; +import { CorrectionInvoiceFormValue, ICorrectionRequest, ReturnFromSaleFormValue } from './models'; import { SharedReturnFormComponent } from './returnForm/form.component'; export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; @@ -68,6 +72,7 @@ export type TSaleInvoiceSingleViewVariant = 'full' | 'customer' | 'pos'; SharedLightBottomsheetComponent, ProgressSpinner, SharedReturnFormComponent, + SharedCorrectionFormComponent, ], }) export class SharedSaleInvoiceSingleViewComponent { @@ -90,18 +95,20 @@ export class SharedSaleInvoiceSingleViewComponent { @Output() onSendToTsp = new EventEmitter>(); @Output() onResendToTsp = new EventEmitter>(); @Output() onInquiry = new EventEmitter>(); + @Output() onCorrection = new EventEmitter(); + @Output() onReturnFromSale = new EventEmitter(); @ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef; moreActionMenuItems = signal([]); showCorrectionForm = signal(false); - showBackFromSaleForm = signal(false); + showReturnFromSaleForm = signal(false); constructor() { this.showErrors = this.showErrors.bind(this); this.showRevokeConfirmation = this.showRevokeConfirmation.bind(this); this.showCorrection = this.showCorrection.bind(this); - this.showBackFromSale = this.showBackFromSale.bind(this); + this.showReturnFromSale = this.showReturnFromSale.bind(this); this.printInvoice = this.printInvoice.bind(this); this.send = this.send.bind(this); this.resend = this.resend.bind(this); @@ -119,9 +126,15 @@ export class SharedSaleInvoiceSingleViewComponent { send() { this.onSendToTsp.emit(); } + sendCorrection(event: ICorrectionRequest) { + this.onCorrection.emit(event); + } resend() { this.onResendToTsp.emit(); } + sendReturnFromSale(event: IPosReturnFromSaleRequest) { + this.onReturnFromSale.emit(event); + } async showRevokeConfirmation() { await this.confirmationService.ask({ header: `ابطال صورت‌حساب‌ شماره ${this.invoice!.invoice_number}`, @@ -143,9 +156,11 @@ export class SharedSaleInvoiceSingleViewComponent { }, }); } - showCorrection() {} - showBackFromSale() { - this.showBackFromSaleForm.set(true); + showCorrection() { + this.showCorrectionForm.set(true); + } + showReturnFromSale() { + this.showReturnFromSaleForm.set(true); } ngOnChanges(changes: SimpleChanges) { @@ -192,7 +207,7 @@ export class SharedSaleInvoiceSingleViewComponent { label: 'برگشت از فروش', icon: 'pi pi-cart-minus', neededStatus: TspProviderResponseStatus.SUCCESS, - command: this.showBackFromSale, + command: this.showReturnFromSale, }, { label: 'چاپ', @@ -461,4 +476,70 @@ export class SharedSaleInvoiceSingleViewComponent { refresh() { this.onRefresh.emit(); } + + private mapCorrectionRequest(event: CorrectionInvoiceFormValue): ICorrectionRequest { + const items = (event.items || []).map(({ id, pricing_model, ...item }) => item); + + const total_amount = items.reduce((sum, item) => sum + Number(item.total_amount || 0), 0); + const discount_amount = items.reduce((sum, item) => sum + Number(item.discount_amount || 0), 0); + const tax_amount = items.reduce((sum, item) => sum + Number(item.tax_amount || 0), 0); + + return { + invoice_date: event.invoice_date, + items, + total_amount, + discount_amount, + tax_amount, + }; + } + + correctionSubmit(event: CorrectionInvoiceFormValue) { + this.sendCorrection(this.mapCorrectionRequest(event)); + } + + private mapReturnFromSaleRequest(event: ReturnFromSaleFormValue): IPosReturnFromSaleRequest { + const invoiceItems = this.invoice?.items || []; + const itemMap = new Map(invoiceItems.map((item) => [item.id, item])); + + const items: IPosOrderItem[] = (event.items || []) + .map((item) => { + const source = itemMap.get(item.id); + if (!source) return null; + + const quantity = Number(item.quantity || 0); + const unit_price = Number(source.unit_price || 0); + const discount_amount = Number(source.discount || 0); + const base_total_amount = unit_price * quantity; + const total_amount = Number(source.total_amount || 0); + const tax_amount = Number(source.payload?.wages || 0); + + return { + good_id: source.good_id, + service_id: source.service_id || undefined, + quantity, + unit_price, + discount_amount, + base_total_amount, + total_amount, + tax_amount, + measure_unit: source.good_snapshot.measure_unit, + payload: source.payload as any, + notes: source.notes, + image_url: source.good_snapshot.image_url, + } as IPosOrderItem; + }) + .filter((item): item is IPosOrderItem => !!item); + + return { + invoice_date: this.invoice?.invoice_date || '', + items, + total_amount: items.reduce((sum, item) => sum + Number(item.total_amount || 0), 0), + discount_amount: items.reduce((sum, item) => sum + Number(item.discount_amount || 0), 0), + tax_amount: items.reduce((sum, item) => sum + Number(item.tax_amount || 0), 0), + }; + } + + returnSubmit(event: ReturnFromSaleFormValue) { + this.sendReturnFromSale(this.mapReturnFromSaleRequest(event)); + } } diff --git a/src/app/shared/components/posPayment/gold-form.component.html b/src/app/shared/components/posPayment/gold-form.component.html index e2d7aa9..f8a3c26 100644 --- a/src/app/shared/components/posPayment/gold-form.component.html +++ b/src/app/shared/components/posPayment/gold-form.component.html @@ -57,6 +57,8 @@ [baseTotalAmount]="baseTotalAmount()" [discountAmount]="form.controls.discount_amount.value || 0" [taxAmount]="taxAmount()" /> - + @if (!isCorrection) { + + } diff --git a/src/app/shared/components/posPayment/gold-form.component.ts b/src/app/shared/components/posPayment/gold-form.component.ts index 4166c63..2fc8fa9 100644 --- a/src/app/shared/components/posPayment/gold-form.component.ts +++ b/src/app/shared/components/posPayment/gold-form.component.ts @@ -42,6 +42,7 @@ export class SharedGoldPayloadFormComponent extends AbstractForm< @Input({ required: true }) vatPercentage!: number; @Input() isRapidInvoice: boolean = false; @Input() ctaText: string = ''; + @Input() isCorrection: boolean = false; @Output() onSubmitForm = new EventEmitter(); @Output() onChangeTotalAmount = new EventEmitter(); @@ -124,8 +125,30 @@ export class SharedGoldPayloadFormComponent extends AbstractForm< form = this.initialForm(); + getCorrectionValue(): IPosOrderItem { + return this.prepareSubmitPayload({ + ...(this.initialValues as IPosOrderItem), + unit_price: Number(this.form.controls.unit_price.value || 0), + quantity: Number(this.form.controls.quantity.value || 0), + discount_amount: Number(this.form.controls.discount_amount.value || 0), + payload: { + ...((this.initialValues as IPosOrderItem)?.payload || {}), + commission: Number(this.form.controls.payload.controls.commission.value || 0), + karat: this.form.controls.payload.controls.karat.value as TGoldKarat, + profit: Number(this.form.controls.payload.controls.profit.value || 0), + wages: Number(this.form.controls.payload.controls.wages.value || 0), + discount_type: + this.form.controls.payload.controls.discount_type.value || goldDiscountType.PROFIT, + }, + } as IPosOrderItem); + } + override submitForm(payload: IPosOrderItem) { - this.onSubmit.emit({ + this.onSubmit.emit(this.prepareSubmitPayload(payload as IPosOrderItem)); + } + + prepareSubmitPayload(payload: IPosOrderItem): IPosOrderItem { + return { ...payload, total_amount: this.totalAmount(), base_total_amount: this.baseTotalAmount(), @@ -139,7 +162,7 @@ export class SharedGoldPayloadFormComponent extends AbstractForm< discount_type: this.form.controls.payload.controls.discount_type.value || goldDiscountType.PROFIT, }, - }); + }; } updateCalculateAmount(payload: Partial>) { diff --git a/src/app/shared/components/posPayment/standard-form.component.html b/src/app/shared/components/posPayment/standard-form.component.html index 4417cf1..39fc798 100644 --- a/src/app/shared/components/posPayment/standard-form.component.html +++ b/src/app/shared/components/posPayment/standard-form.component.html @@ -24,6 +24,8 @@ [baseTotalAmount]="baseTotalAmount()" [discountAmount]="discountAmount()" [taxAmount]="taxAmount()" /> - + @if (!isCorrection) { + + } diff --git a/src/app/shared/components/posPayment/standard-form.component.ts b/src/app/shared/components/posPayment/standard-form.component.ts index a0e8fc3..517a67b 100644 --- a/src/app/shared/components/posPayment/standard-form.component.ts +++ b/src/app/shared/components/posPayment/standard-form.component.ts @@ -34,6 +34,7 @@ export class SharedStandardPayloadFormComponent extends AbstractForm< @Input({ required: true }) vatPercentage!: number; @Input() isRapidInvoice: boolean = false; @Input() ctaText: string = ''; + @Input() isCorrection: boolean = false; @Output() onChangeTotalAmount = new EventEmitter(); private readonly initialForm = () => { @@ -55,14 +56,29 @@ export class SharedStandardPayloadFormComponent extends AbstractForm< }; form = this.initialForm(); - override submitForm(payload: IPosOrderItem) { - this.onSubmit.emit({ + + getCorrectionValue(): IPosOrderItem { + return this.prepareSubmitPayload({ + ...(this.initialValues as IPosOrderItem), + unit_price: Number(this.form.controls.unit_price.value || 0), + quantity: Number(this.form.controls.quantity.value || 0), + discount_amount: Number(this.form.controls.discount_amount.value || 0), + } as IPosOrderItem); + } + + override submitForm(payload: IPosOrderItem) { + this.onSubmit.emit(this.prepareSubmitPayload(payload as IPosOrderItem)); + } + + prepareSubmitPayload(payload: IPosOrderItem): IPosOrderItem { + return { ...payload, total_amount: this.totalAmount(), - discount_amount: this.discountAmount(), - tax_amount: this.taxAmount(), base_total_amount: this.baseTotalAmount(), - }); + tax_amount: this.taxAmount(), + discount_amount: this.form.value.discount_amount || 0, + payload: {}, + }; } baseTotalAmount = signal(0); @@ -88,5 +104,6 @@ export class SharedStandardPayloadFormComponent extends AbstractForm< this.discountAmount.set(discountAmount); this.taxAmount.set(taxAmount); this.totalAmount.set(baseTotalAmountWithoutTax + taxAmount); + this.onChangeTotalAmount.emit(this.totalAmount()); } } diff --git a/src/app/uikit/datepicker/datepicker.component.html b/src/app/uikit/datepicker/datepicker.component.html index b0c5340..f22730d 100644 --- a/src/app/uikit/datepicker/datepicker.component.html +++ b/src/app/uikit/datepicker/datepicker.component.html @@ -43,7 +43,7 @@ [ngClass]="{ 'bg-surface-ground font-bold': day.isCurrentDay, 'bg-primary! text-primary-contrast!': isSelected(day), - 'text-muted-color! font-light': day.isDisabled, + 'text-muted-color! font-light opacity-50': day.isDisabled, }" (click)="selectDate(day)"> {{ day.day }}