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.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<field-invoice-date [control]="form.controls.invoice_date" />
|
||||
|
||||
@for (item of initialValues || []; track item.id) {
|
||||
<div class="py-3">
|
||||
@if ($index) {
|
||||
<hr class="h-1 w-full" />
|
||||
}
|
||||
<p-divider align="right">
|
||||
{{ $index + 1 }}- <b>{{ item.good_snapshot.name }}</b>
|
||||
</p-divider>
|
||||
@if (isGold(item)) {
|
||||
<shared-gold-payload-form
|
||||
[initialValues]="$any(mappedInitialItems[$index])"
|
||||
[vatPercentage]="vatFromItem(item)"
|
||||
[isCorrection]="true" />
|
||||
} @else {
|
||||
<shared-standard-payload-form
|
||||
[initialValues]="$any(mappedInitialItems[$index])"
|
||||
[measureUnit]="item.good_snapshot.measure_unit"
|
||||
[vatPercentage]="vatFromItem(item)"
|
||||
[isCorrection]="true" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-form-footer-actions />
|
||||
</form>
|
||||
@@ -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<SharedGoldPayloadFormComponent>;
|
||||
@ViewChildren(SharedStandardPayloadFormComponent)
|
||||
standardForms!: QueryList<SharedStandardPayloadFormComponent>;
|
||||
|
||||
form = this.fb.group({
|
||||
invoice_date: [this.invoiceDate],
|
||||
});
|
||||
|
||||
itemDrafts: Record<string, TCorrectionItemPayload> = {};
|
||||
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<TCorrectionItemPayload, 'payload'> {
|
||||
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<IGoldPayload> {
|
||||
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<IStandardPayload> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './form.component';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IPosOrderItem } from '@/domains/pos/modules/shop/models';
|
||||
import { IGoldPayload, IStandardPayload } from '@/shared/models';
|
||||
|
||||
export type TCorrectionItemPayload = IPosOrderItem<IGoldPayload | IStandardPayload> & {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './correction';
|
||||
export * from './return';
|
||||
@@ -0,0 +1,7 @@
|
||||
export type ReturnFromSaleFormValue = {
|
||||
items: {
|
||||
id: string;
|
||||
quantity: number;
|
||||
maxQuantity: number;
|
||||
}[];
|
||||
};
|
||||
@@ -1,7 +1,10 @@
|
||||
<form [formGroup]="form" (ngSubmit)="submit()">
|
||||
<p-message severity="info" icon="pi pi-info-circle">
|
||||
در برگشت از فروش صورتحساب شما میتوانید صرفا تعداد محصولات و خدمات خود را کم و حذف کنید و حداقل مقدار محصولات و خدمات
|
||||
شما ۱ می باشد.
|
||||
<p-message severity="info">
|
||||
<ol class="list-disc ps-4">
|
||||
<li>تعداد محصولات/خدمات خود را کم و یا حذف کنید</li>
|
||||
<li>حداقل ۱ خدمت/سرویس بایستی باقی بماند</li>
|
||||
<li>تخفیفها، مالیات و قیمت نهایی تمام شده با توجه به مقادیر فاکتور اصلی به صورت سیستمی محاسبه خواهند شد.</li>
|
||||
</ol>
|
||||
</p-message>
|
||||
|
||||
<field-invoice-date [control]="form.controls.invoice_date" />
|
||||
|
||||
@@ -10,7 +10,9 @@ import { IInvoiceItem } from '../sale-invoice-full-response.model';
|
||||
import { SharedReturnFormItemComponent } from './item-form.component';
|
||||
|
||||
type ItemForm = FormGroup<{
|
||||
id: FormControl<string | null>;
|
||||
quantity: FormControl<number | null>;
|
||||
maxQuantity: FormControl<number | null>;
|
||||
}>;
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface ISaleInvoiceFullRawResponse {
|
||||
main_id: Maybe<string>;
|
||||
tax_id: Maybe<string>;
|
||||
reference_invoice: Maybe<string>;
|
||||
refrence_by: Maybe<string>;
|
||||
notes: Maybe<string>;
|
||||
settlement_type: IEnumTranslate<InvoiceSettlementType>;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,16 @@
|
||||
</app-card-data>
|
||||
</div>
|
||||
|
||||
<shared-light-bottomsheet [(visible)]="showBackFromSaleForm" header=" برگشت از فروش">
|
||||
<shared-return-form [initialValues]="invoice.items" [invoiceDate]="invoice.invoice_date" />
|
||||
<shared-light-bottomsheet [(visible)]="showReturnFromSaleForm" header=" برگشت از فروش">
|
||||
<shared-return-form
|
||||
[initialValues]="invoice.items"
|
||||
[invoiceDate]="invoice.invoice_date"
|
||||
(onSubmit)="returnSubmit($event)" />
|
||||
</shared-light-bottomsheet>
|
||||
<shared-light-bottomsheet [(visible)]="showCorrectionForm" header="اصلاحی">
|
||||
<shared-correction-form
|
||||
[initialValues]="invoice.items"
|
||||
[invoiceDate]="invoice.invoice_date"
|
||||
(onSubmit)="correctionSubmit($event)" />
|
||||
</shared-light-bottomsheet>
|
||||
}
|
||||
|
||||
@@ -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<Observable<any>>();
|
||||
@Output() onResendToTsp = new EventEmitter<Observable<any>>();
|
||||
@Output() onInquiry = new EventEmitter<Observable<any>>();
|
||||
@Output() onCorrection = new EventEmitter<ICorrectionRequest>();
|
||||
@Output() onReturnFromSale = new EventEmitter<IPosReturnFromSaleRequest>();
|
||||
|
||||
@ViewChild('totalAmount', { static: false }) totalAmount!: TemplateRef<any>;
|
||||
|
||||
moreActionMenuItems = signal<MenuItem[]>([]);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user