.
This commit is contained in:
parent
9931a4a839
commit
26bc608131
@ -66,10 +66,28 @@ export const QuotesDataTable = ({
|
|||||||
|
|
||||||
const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, any>[]>(
|
const columns = useMemo<ColumnDef<IListQuotes_Response_DTO, any>[]>(
|
||||||
() => [
|
() => [
|
||||||
|
{
|
||||||
|
id: "reference" as const,
|
||||||
|
accessorKey: "reference",
|
||||||
|
header: () => <>{t("quotes.list.columns.reference")}</>,
|
||||||
|
cell: ({ renderValue }) => <div className='text-left text-ellipsis'>{renderValue()}</div>,
|
||||||
|
enableResizing: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "status" as const,
|
||||||
|
accessorKey: "status",
|
||||||
|
header: () => <>{t("quotes.list.columns.status")}</>,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
cell: ({ row: { original } }) => <ColorBadge label={original.status} />,
|
||||||
|
enableResizing: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "date" as const,
|
id: "date" as const,
|
||||||
accessor: "date",
|
accessor: "date",
|
||||||
header: () => <>{t("quotes.list.columns.date")}</>,
|
header: () => (
|
||||||
|
<div className='text-right text-ellipsis'>{t("quotes.list.columns.date")}</div>
|
||||||
|
),
|
||||||
cell: ({ row: { original } }) => {
|
cell: ({ row: { original } }) => {
|
||||||
const quoteDate = UTCDateValue.create(original.date);
|
const quoteDate = UTCDateValue.create(original.date);
|
||||||
return (
|
return (
|
||||||
@ -80,6 +98,13 @@ export const QuotesDataTable = ({
|
|||||||
},
|
},
|
||||||
enableResizing: false,
|
enableResizing: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "customer_reference" as const,
|
||||||
|
accessorKey: "customer_reference",
|
||||||
|
header: () => <>{t("quotes.list.columns.customer_reference")}</>,
|
||||||
|
cell: ({ renderValue }) => <div className='text-left text-ellipsis'>{renderValue()}</div>,
|
||||||
|
enableResizing: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "customer_information" as const,
|
id: "customer_information" as const,
|
||||||
accessorKey: "customer_information",
|
accessorKey: "customer_information",
|
||||||
@ -104,21 +129,7 @@ export const QuotesDataTable = ({
|
|||||||
enableResizing: false,
|
enableResizing: false,
|
||||||
size: 640,
|
size: 640,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "reference" as const,
|
|
||||||
accessorKey: "reference",
|
|
||||||
header: () => <>{t("quotes.list.columns.reference")}</>,
|
|
||||||
cell: ({ renderValue }) => <div className='text-left text-ellipsis'>{renderValue()}</div>,
|
|
||||||
enableResizing: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "status" as const,
|
|
||||||
accessorKey: "status",
|
|
||||||
header: () => <>{t("quotes.list.columns.status")}</>,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
cell: ({ row: { original } }) => <ColorBadge label={original.status} />,
|
|
||||||
enableResizing: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "total_price" as const,
|
id: "total_price" as const,
|
||||||
accessor: "total_price",
|
accessor: "total_price",
|
||||||
|
|||||||
@ -27,11 +27,11 @@ export const QuoteGeneralCardEditor = () => {
|
|||||||
errors={formState.errors}
|
errors={formState.errors}
|
||||||
/>
|
/>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
label={t("quotes.form_fields.reference.label")}
|
label={t("quotes.form_fields.customer_reference.label")}
|
||||||
description={t("quotes.form_fields.reference.desc")}
|
description={t("quotes.form_fields.customer_reference.desc")}
|
||||||
disabled={formState.disabled}
|
disabled={formState.disabled}
|
||||||
placeholder={t("quotes.form_fields.reference.placeholder")}
|
placeholder={t("quotes.form_fields.customer_reference.placeholder")}
|
||||||
{...register("reference", {
|
{...register("customer_reference", {
|
||||||
required: false,
|
required: false,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -29,8 +29,9 @@ export const QuoteCreate = () => {
|
|||||||
const defaultValues = useMemo(
|
const defaultValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
date: new Date(Date.now()).toUTCString(),
|
date: new Date(Date.now()).toUTCString(),
|
||||||
|
customer_reference: "",
|
||||||
customer_information: "",
|
customer_information: "",
|
||||||
reference: "",
|
//reference: "",
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@ -40,9 +41,10 @@ export const QuoteCreate = () => {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
resolver: joiResolver(
|
resolver: joiResolver(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
reference: Joi.string().required(),
|
//reference: Joi.string().required(),
|
||||||
date: Joi.date().required(),
|
|
||||||
customer_information: Joi.string().required(),
|
customer_information: Joi.string().required(),
|
||||||
|
date: Joi.date().required(),
|
||||||
|
customer_reference: Joi.string(),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
//messages: SpanishJoiMessages,
|
//messages: SpanishJoiMessages,
|
||||||
@ -99,13 +101,22 @@ export const QuoteCreate = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid w-6/12 gap-6 mx-auto'>
|
<div className='grid w-6/12 gap-6 mx-auto'>
|
||||||
<FormTextField
|
{/*<FormTextField
|
||||||
className='row-span-2'
|
className='row-span-2'
|
||||||
name='reference'
|
name='reference'
|
||||||
required
|
required
|
||||||
label={t("quotes.form_fields.reference.label")}
|
label={t("quotes.form_fields.reference.label")}
|
||||||
description={t("quotes.form_fields.reference.desc")}
|
description={t("quotes.form_fields.reference.desc")}
|
||||||
placeholder={t("quotes.form_fields.reference.placeholder")}
|
placeholder={t("quotes.form_fields.reference.placeholder")}
|
||||||
|
/>*/}
|
||||||
|
|
||||||
|
<FormTextField
|
||||||
|
className='row-span-2'
|
||||||
|
name='customer_reference'
|
||||||
|
required
|
||||||
|
label={t("quotes.form_fields.customer_reference.label")}
|
||||||
|
description={t("quotes.form_fields.customer_reference.desc")}
|
||||||
|
placeholder={t("quotes.form_fields.customer_reference.placeholder")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormDatePickerField
|
<FormDatePickerField
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export const QuoteEdit = () => {
|
|||||||
() => ({
|
() => ({
|
||||||
date: "",
|
date: "",
|
||||||
reference: "",
|
reference: "",
|
||||||
|
customer_reference: "",
|
||||||
customer_information: "",
|
customer_information: "",
|
||||||
lang_code: "",
|
lang_code: "",
|
||||||
currency_code: "",
|
currency_code: "",
|
||||||
@ -244,7 +245,7 @@ export const QuoteEdit = () => {
|
|||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<BackHistoryButton />
|
<BackHistoryButton />
|
||||||
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
||||||
{t("quotes.edit.title")}
|
{t("quotes.edit.title")} {data.reference}
|
||||||
</h1>
|
</h1>
|
||||||
<ColorBadge label={data.status} className='ml-auto sm:ml-0' />
|
<ColorBadge label={data.status} className='ml-auto sm:ml-0' />
|
||||||
|
|
||||||
|
|||||||
@ -279,9 +279,9 @@ export const useQuotes = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const download = (id: string, filename?: string) => {
|
const download = (id: string, filename: string) => {
|
||||||
const url = actions.getQuotePDFDownloadURL(id);
|
const url = actions.getQuotePDFDownloadURL(id);
|
||||||
return downloader.download(url, filename ?? "ssaas");
|
return downloader.download(url, filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -112,6 +112,7 @@
|
|||||||
"date": "Date",
|
"date": "Date",
|
||||||
"reference": "Reference",
|
"reference": "Reference",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
|
"customer_reference": "Customer Ref.",
|
||||||
"customer_information": "Customer",
|
"customer_information": "Customer",
|
||||||
"total_price": "Imp. total",
|
"total_price": "Imp. total",
|
||||||
"actions": {
|
"actions": {
|
||||||
@ -197,6 +198,11 @@
|
|||||||
"desc": "Quote currency",
|
"desc": "Quote currency",
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
},
|
},
|
||||||
|
"customer_reference": {
|
||||||
|
"label": "Customer reference",
|
||||||
|
"desc": "Customer reference for this quote",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
"customer_information": {
|
"customer_information": {
|
||||||
"label": "Customer's contact data",
|
"label": "Customer's contact data",
|
||||||
"desc": "Recommendation: enter the customer's name on the first line, the address on the second line, and the zip code and city/state on the third line.",
|
"desc": "Recommendation: enter the customer's name on the first line, the address on the second line, and the zip code and city/state on the third line.",
|
||||||
@ -205,7 +211,7 @@
|
|||||||
"payment_method": {
|
"payment_method": {
|
||||||
"label": "Payment method",
|
"label": "Payment method",
|
||||||
"placeholder": "",
|
"placeholder": "",
|
||||||
"desc": "Method of payment of the quote"
|
"desc": "Method of payment for this quote"
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"label": "Notes",
|
"label": "Notes",
|
||||||
|
|||||||
@ -194,6 +194,11 @@
|
|||||||
"desc": "Moneda de la cotización",
|
"desc": "Moneda de la cotización",
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
},
|
},
|
||||||
|
"customer_reference": {
|
||||||
|
"label": "Referencia del cliente",
|
||||||
|
"desc": "Referencia para el cliente de esta cotización",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
"customer_information": {
|
"customer_information": {
|
||||||
"label": "Datos del cliente",
|
"label": "Datos del cliente",
|
||||||
"desc": "Recomensación: escriba el nombre del cliente en la primera línea, la direccion en la segunda y el código postal y ciudad en la tercera.",
|
"desc": "Recomensación: escriba el nombre del cliente en la primera línea, la direccion en la segunda y el código postal y ciudad en la tercera.",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/applica
|
|||||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||||
|
import { SequelizeBusinessTransactionType } from "@/contexts/common/infrastructure/sequelize/SequelizeBusinessTransaction";
|
||||||
import {
|
import {
|
||||||
Collection,
|
Collection,
|
||||||
CurrencyData,
|
CurrencyData,
|
||||||
@ -25,11 +26,13 @@ import {
|
|||||||
IQuoteRepository,
|
IQuoteRepository,
|
||||||
Quote,
|
Quote,
|
||||||
QuoteCustomer,
|
QuoteCustomer,
|
||||||
|
QuoteCustomerReference,
|
||||||
QuoteItem,
|
QuoteItem,
|
||||||
QuoteReference,
|
QuoteReference,
|
||||||
QuoteStatus,
|
QuoteStatus,
|
||||||
} from "../../domain";
|
} from "../../domain";
|
||||||
import { ISalesContext } from "../../infrastructure";
|
import { ISalesContext } from "../../infrastructure";
|
||||||
|
import { generateQuoteReferenceForDealer } from "../services";
|
||||||
|
|
||||||
export type CreateQuoteResponseOrError =
|
export type CreateQuoteResponseOrError =
|
||||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||||
@ -40,12 +43,13 @@ export class CreateQuoteUseCase
|
|||||||
{
|
{
|
||||||
private _adapter: ISequelizeAdapter;
|
private _adapter: ISequelizeAdapter;
|
||||||
private _repositoryManager: IRepositoryManager;
|
private _repositoryManager: IRepositoryManager;
|
||||||
private _dealer?: Dealer;
|
private _dealer: Dealer;
|
||||||
|
private _transaction: SequelizeBusinessTransactionType;
|
||||||
|
|
||||||
constructor(context: ISalesContext) {
|
constructor(context: ISalesContext) {
|
||||||
this._adapter = context.adapter;
|
this._adapter = context.adapter;
|
||||||
this._repositoryManager = context.repositoryManager;
|
this._repositoryManager = context.repositoryManager;
|
||||||
this._dealer = context.dealer;
|
this._dealer = context.dealer!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(request: ICreateQuote_Request_DTO) {
|
async execute(request: ICreateQuote_Request_DTO) {
|
||||||
@ -57,8 +61,6 @@ export class CreateQuoteUseCase
|
|||||||
return Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message));
|
return Result.fail(UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
const dealerId = this._dealer.id;
|
|
||||||
|
|
||||||
const idOrError = ensureIdIsValid(id);
|
const idOrError = ensureIdIsValid(id);
|
||||||
if (idOrError.isFailure) {
|
if (idOrError.isFailure) {
|
||||||
const message = idOrError.error.message; //`Quote ID ${quoteDTO.id} is not valid`;
|
const message = idOrError.error.message; //`Quote ID ${quoteDTO.id} is not valid`;
|
||||||
@ -67,58 +69,58 @@ export class CreateQuoteUseCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprobar que no existe un quote previo con esos datos
|
this._transaction = this._adapter.startTransaction();
|
||||||
const quoteRepository = this._getQuoteRepository();
|
const quoteRepository = this._getQuoteRepository();
|
||||||
|
|
||||||
const idExists = await quoteRepository().exists(idOrError.object);
|
|
||||||
if (idExists) {
|
|
||||||
const message = `Another quote with same ID exists`;
|
|
||||||
return Result.fail(
|
|
||||||
UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
|
|
||||||
path: "id",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crear quote
|
|
||||||
const quoteOrError = this._tryCreateQuoteInstance(request, idOrError.object, dealerId);
|
|
||||||
|
|
||||||
if (quoteOrError.isFailure) {
|
|
||||||
const { error: domainError } = quoteOrError;
|
|
||||||
let errorCode = "";
|
|
||||||
let message = "";
|
|
||||||
|
|
||||||
switch (domainError.code) {
|
|
||||||
case DomainError.INVALID_INPUT_DATA:
|
|
||||||
errorCode = UseCaseError.INVALID_INPUT_DATA;
|
|
||||||
message = "El presupuesto tiene algún dato erróneo.";
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
errorCode = UseCaseError.UNEXCEPTED_ERROR;
|
|
||||||
message = domainError.message;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.fail(UseCaseError.create(errorCode, message, domainError));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._saveQuote(quoteOrError.object);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _saveQuote(quote: Quote) {
|
|
||||||
// Guardar el contacto
|
|
||||||
const transaction = this._adapter.startTransaction();
|
|
||||||
const quoteRepository = this._getQuoteRepository();
|
|
||||||
let quoteRepo: IQuoteRepository;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await transaction.complete(async (t) => {
|
return await this._transaction.complete(async (t) => {
|
||||||
quoteRepo = quoteRepository({ transaction: t });
|
const quoteRepo = quoteRepository({ transaction: t });
|
||||||
await quoteRepo.create(quote);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Result.ok<Quote>(quote);
|
// Comprobar que no existe un quote previo con esos datos
|
||||||
|
const idExists = await quoteRepo.exists(idOrError.object);
|
||||||
|
if (idExists) {
|
||||||
|
const message = `Another quote with same ID exists`;
|
||||||
|
return Result.fail(
|
||||||
|
UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
|
||||||
|
path: "id",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Reference
|
||||||
|
const quoteReference = await generateQuoteReferenceForDealer(this._dealer, quoteRepo);
|
||||||
|
|
||||||
|
// Crear quote
|
||||||
|
const quoteOrError = this._tryCreateQuoteInstance(
|
||||||
|
request,
|
||||||
|
idOrError.object,
|
||||||
|
quoteReference
|
||||||
|
);
|
||||||
|
|
||||||
|
if (quoteOrError.isFailure) {
|
||||||
|
const { error: domainError } = quoteOrError;
|
||||||
|
let errorCode = "";
|
||||||
|
let message = "";
|
||||||
|
|
||||||
|
switch (domainError.code) {
|
||||||
|
case DomainError.INVALID_INPUT_DATA:
|
||||||
|
errorCode = UseCaseError.INVALID_INPUT_DATA;
|
||||||
|
message = "El presupuesto tiene algún dato erróneo.";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
errorCode = UseCaseError.UNEXCEPTED_ERROR;
|
||||||
|
message = domainError.message;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.fail(UseCaseError.create(errorCode, message, domainError));
|
||||||
|
}
|
||||||
|
|
||||||
|
await quoteRepo.create(quoteOrError.object);
|
||||||
|
|
||||||
|
return Result.ok<Quote>(quoteOrError.object);
|
||||||
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const _error = error as IInfrastructureError;
|
const _error = error as IInfrastructureError;
|
||||||
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
|
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
|
||||||
@ -128,8 +130,10 @@ export class CreateQuoteUseCase
|
|||||||
private _tryCreateQuoteInstance(
|
private _tryCreateQuoteInstance(
|
||||||
quoteDTO: ICreateQuote_Request_DTO,
|
quoteDTO: ICreateQuote_Request_DTO,
|
||||||
quoteId: UniqueID,
|
quoteId: UniqueID,
|
||||||
dealerId: UniqueID
|
quoteReference: QuoteReference
|
||||||
): Result<Quote, IDomainError> {
|
): Result<Quote, IDomainError> {
|
||||||
|
const dealerId: UniqueID = this._dealer.id;
|
||||||
|
|
||||||
const statusOrError = QuoteStatus.create(quoteDTO.status);
|
const statusOrError = QuoteStatus.create(quoteDTO.status);
|
||||||
if (statusOrError.isFailure) {
|
if (statusOrError.isFailure) {
|
||||||
return Result.fail(statusOrError.error);
|
return Result.fail(statusOrError.error);
|
||||||
@ -140,11 +144,6 @@ export class CreateQuoteUseCase
|
|||||||
return Result.fail(dateOrError.error);
|
return Result.fail(dateOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceOrError = QuoteReference.create(quoteDTO.reference);
|
|
||||||
if (referenceOrError.isFailure) {
|
|
||||||
return Result.fail(referenceOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageOrError = Language.createFromCode(
|
const languageOrError = Language.createFromCode(
|
||||||
quoteDTO.lang_code ?? this._dealer?.language.code
|
quoteDTO.lang_code ?? this._dealer?.language.code
|
||||||
);
|
);
|
||||||
@ -152,6 +151,11 @@ export class CreateQuoteUseCase
|
|||||||
return Result.fail(languageOrError.error);
|
return Result.fail(languageOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customerReferenceOrError = QuoteCustomerReference.create(quoteDTO.customer_reference);
|
||||||
|
if (customerReferenceOrError.isFailure) {
|
||||||
|
return Result.fail(customerReferenceOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
const customerOrError = QuoteCustomer.create(quoteDTO.customer_information);
|
const customerOrError = QuoteCustomer.create(quoteDTO.customer_information);
|
||||||
if (customerOrError.isFailure) {
|
if (customerOrError.isFailure) {
|
||||||
return Result.fail(customerOrError.error);
|
return Result.fail(customerOrError.error);
|
||||||
@ -259,8 +263,9 @@ export class CreateQuoteUseCase
|
|||||||
{
|
{
|
||||||
status: statusOrError.object,
|
status: statusOrError.object,
|
||||||
date: dateOrError.object,
|
date: dateOrError.object,
|
||||||
reference: referenceOrError.object,
|
reference: quoteReference,
|
||||||
language: languageOrError.object,
|
language: languageOrError.object,
|
||||||
|
customerReference: customerReferenceOrError.object,
|
||||||
customer: customerOrError.object,
|
customer: customerOrError.object,
|
||||||
currency: currencyOrError.object,
|
currency: currencyOrError.object,
|
||||||
paymentMethod: paymentOrError.object,
|
paymentMethod: paymentOrError.object,
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
IQuoteRepository,
|
IQuoteRepository,
|
||||||
Quote,
|
Quote,
|
||||||
QuoteCustomer,
|
QuoteCustomer,
|
||||||
|
QuoteCustomerReference,
|
||||||
QuoteItem,
|
QuoteItem,
|
||||||
QuoteReference,
|
QuoteReference,
|
||||||
QuoteStatus,
|
QuoteStatus,
|
||||||
@ -153,6 +154,11 @@ export class UpdateQuoteUseCase
|
|||||||
return Result.fail(languageOrError.error);
|
return Result.fail(languageOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customerReferenceOrError = QuoteCustomerReference.create(quoteDTO.customer_reference);
|
||||||
|
if (customerReferenceOrError.isFailure) {
|
||||||
|
return Result.fail(customerReferenceOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
const customerOrError = QuoteCustomer.create(quoteDTO.customer_information);
|
const customerOrError = QuoteCustomer.create(quoteDTO.customer_information);
|
||||||
if (customerOrError.isFailure) {
|
if (customerOrError.isFailure) {
|
||||||
return Result.fail(customerOrError.error);
|
return Result.fail(customerOrError.error);
|
||||||
@ -253,6 +259,7 @@ export class UpdateQuoteUseCase
|
|||||||
date: dateOrError.object,
|
date: dateOrError.object,
|
||||||
reference: referenceOrError.object,
|
reference: referenceOrError.object,
|
||||||
language: languageOrError.object,
|
language: languageOrError.object,
|
||||||
|
customerReference: customerReferenceOrError.object,
|
||||||
customer: customerOrError.object,
|
customer: customerOrError.object,
|
||||||
currency: currencyOrError.object,
|
currency: currencyOrError.object,
|
||||||
paymentMethod: paymentOrError.object,
|
paymentMethod: paymentOrError.object,
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { IDealer, IQuoteRepository, QuoteReference } from "../../domain";
|
||||||
|
|
||||||
|
const generateQuoteReferenceForDealer = async (
|
||||||
|
dealer: IDealer,
|
||||||
|
quoteRepository: IQuoteRepository
|
||||||
|
): Promise<QuoteReference> => {
|
||||||
|
// Obtener la última referencia del dealer
|
||||||
|
const lastQuote = await quoteRepository.findLastQuoteByDealerId(dealer.id);
|
||||||
|
|
||||||
|
// Generar la nueva referencia usando el Value Object
|
||||||
|
const quoteReferenceResult = QuoteReference.fromPrevious(
|
||||||
|
lastQuote ? lastQuote.reference : null,
|
||||||
|
dealer
|
||||||
|
);
|
||||||
|
|
||||||
|
if (quoteReferenceResult.isFailure) {
|
||||||
|
throw new Error("Error al crear la referencia de presupuesto");
|
||||||
|
}
|
||||||
|
|
||||||
|
return quoteReferenceResult.object;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { generateQuoteReferenceForDealer };
|
||||||
1
server/src/contexts/sales/application/services/index.ts
Normal file
1
server/src/contexts/sales/application/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./QuoteService";
|
||||||
@ -29,6 +29,8 @@ export interface IDealer {
|
|||||||
additionalInfo: KeyValueMap;
|
additionalInfo: KeyValueMap;
|
||||||
status: DealerStatus;
|
status: DealerStatus;
|
||||||
currency: CurrencyData;
|
currency: CurrencyData;
|
||||||
|
|
||||||
|
getAcronym: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
|
export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
|
||||||
@ -37,6 +39,12 @@ export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
|
|||||||
return Result.ok<Dealer>(user);
|
return Result.ok<Dealer>(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static generateDealerNameAcronyn(dealerName: Name | string): string {
|
||||||
|
return typeof dealerName === "string"
|
||||||
|
? Name.generateAcronym(dealerName)
|
||||||
|
: dealerName.getAcronym();
|
||||||
|
}
|
||||||
|
|
||||||
get user_id(): UniqueID {
|
get user_id(): UniqueID {
|
||||||
return this.props.user_id;
|
return this.props.user_id;
|
||||||
}
|
}
|
||||||
@ -60,4 +68,8 @@ export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
|
|||||||
get currency(): CurrencyData {
|
get currency(): CurrencyData {
|
||||||
return this.props.currency;
|
return this.props.currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAcronym(): string {
|
||||||
|
return this.props.name.getAcronym();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export interface IQuoteProps {
|
|||||||
status: QuoteStatus;
|
status: QuoteStatus;
|
||||||
date: UTCDateValue;
|
date: UTCDateValue;
|
||||||
reference: QuoteReference;
|
reference: QuoteReference;
|
||||||
|
customerReference: QuoteReference;
|
||||||
customer: QuoteCustomer;
|
customer: QuoteCustomer;
|
||||||
language: Language;
|
language: Language;
|
||||||
currency: CurrencyData;
|
currency: CurrencyData;
|
||||||
@ -43,6 +44,7 @@ export interface IQuote {
|
|||||||
status: QuoteStatus;
|
status: QuoteStatus;
|
||||||
date: UTCDateValue;
|
date: UTCDateValue;
|
||||||
reference: QuoteReference;
|
reference: QuoteReference;
|
||||||
|
customerReference: QuoteReference;
|
||||||
customer: QuoteCustomer;
|
customer: QuoteCustomer;
|
||||||
language: Language;
|
language: Language;
|
||||||
currency: CurrencyData;
|
currency: CurrencyData;
|
||||||
@ -115,6 +117,10 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
|||||||
return this.props.reference;
|
return this.props.reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get customerReference() {
|
||||||
|
return this.props.customerReference;
|
||||||
|
}
|
||||||
|
|
||||||
get customer() {
|
get customer() {
|
||||||
return this.props.customer;
|
return this.props.customer;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export class QuoteCustomer extends StringValueObject {
|
|||||||
.default("")
|
.default("")
|
||||||
.trim()
|
.trim()
|
||||||
.max(QuoteCustomer.MAX_LENGTH)
|
.max(QuoteCustomer.MAX_LENGTH)
|
||||||
.label(options.label ? options.label : "value");
|
.label(options.label ? options.label : "customer_information");
|
||||||
|
|
||||||
return RuleValidator.validate<string>(rule, value);
|
return RuleValidator.validate<string>(rule, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
DomainError,
|
||||||
|
IStringValueObjectOptions,
|
||||||
|
Result,
|
||||||
|
RuleValidator,
|
||||||
|
StringValueObject,
|
||||||
|
handleDomainError,
|
||||||
|
} from "@shared/contexts";
|
||||||
|
import { UndefinedOr } from "@shared/utilities";
|
||||||
|
import Joi from "joi";
|
||||||
|
|
||||||
|
export interface IQuoteCustomerReferenceOptions extends IStringValueObjectOptions {}
|
||||||
|
|
||||||
|
export class QuoteCustomerReference extends StringValueObject {
|
||||||
|
private static readonly MAX_LENGTH = 255;
|
||||||
|
|
||||||
|
protected static validate(value: UndefinedOr<string>, options: IQuoteCustomerReferenceOptions) {
|
||||||
|
const rule = Joi.string()
|
||||||
|
.allow(null)
|
||||||
|
.allow("")
|
||||||
|
.default("")
|
||||||
|
.trim()
|
||||||
|
.max(QuoteCustomerReference.MAX_LENGTH)
|
||||||
|
.label(options.label ? options.label : "value");
|
||||||
|
|
||||||
|
return RuleValidator.validate<string>(rule, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(value: UndefinedOr<string>, options: IQuoteCustomerReferenceOptions = {}) {
|
||||||
|
const _options = {
|
||||||
|
label: "customer_reference",
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const validationResult = QuoteCustomerReference.validate(value, _options);
|
||||||
|
|
||||||
|
if (validationResult.isFailure) {
|
||||||
|
return Result.fail(
|
||||||
|
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(new QuoteCustomerReference(validationResult.object));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,8 +6,9 @@ import {
|
|||||||
StringValueObject,
|
StringValueObject,
|
||||||
handleDomainError,
|
handleDomainError,
|
||||||
} from "@shared/contexts";
|
} from "@shared/contexts";
|
||||||
import { UndefinedOr } from "@shared/utilities";
|
import { NullOr, UndefinedOr } from "@shared/utilities";
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
import { IDealer } from "../Dealer";
|
||||||
|
|
||||||
export interface IQuoteReferenceOptions extends IStringValueObjectOptions {}
|
export interface IQuoteReferenceOptions extends IStringValueObjectOptions {}
|
||||||
|
|
||||||
@ -42,4 +43,24 @@ export class QuoteReference extends StringValueObject {
|
|||||||
|
|
||||||
return Result.ok(new QuoteReference(validationResult.object));
|
return Result.ok(new QuoteReference(validationResult.object));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static fromPrevious(
|
||||||
|
previousReference: NullOr<QuoteReference>,
|
||||||
|
dealer: IDealer
|
||||||
|
): Result<QuoteReference> {
|
||||||
|
const lastNumber = previousReference
|
||||||
|
? QuoteReference._extractSequenceFromReference(previousReference)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const newNumber = lastNumber + 1;
|
||||||
|
const dealerAcronym = dealer.getAcronym();
|
||||||
|
const newReference = `${dealerAcronym}-${newNumber.toString().padStart(4, "0")}`;
|
||||||
|
|
||||||
|
return QuoteReference.create(newReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _extractSequenceFromReference(reference: QuoteReference): number {
|
||||||
|
const parts = reference.toString().split("-");
|
||||||
|
return parseInt(parts[parts.length - 1], 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./Quote";
|
export * from "./Quote";
|
||||||
export * from "./QuoteCustomer";
|
export * from "./QuoteCustomer";
|
||||||
|
export * from "./QuoteCustomerReference";
|
||||||
export * from "./QuoteItem";
|
export * from "./QuoteItem";
|
||||||
export * from "./QuoteReference";
|
export * from "./QuoteReference";
|
||||||
export * from "./QuoteStatus";
|
export * from "./QuoteStatus";
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./entities";
|
export * from "./entities";
|
||||||
export * from "./repository";
|
export * from "./repository";
|
||||||
|
export * from "./services";
|
||||||
|
|||||||
@ -12,4 +12,6 @@ export interface IQuoteRepository extends IRepository<Quote> {
|
|||||||
findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<Quote>>;
|
findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<Quote>>;
|
||||||
|
|
||||||
removeById(id: UniqueID): Promise<void>;
|
removeById(id: UniqueID): Promise<void>;
|
||||||
|
|
||||||
|
findLastQuoteByDealerId(dealerId: UniqueID): Promise<Quote | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { IApplicationService } from "@/contexts/common/application";
|
||||||
|
|
||||||
|
export interface IQuoteReferenceGeneratorService extends IApplicationService {}
|
||||||
1
server/src/contexts/sales/domain/services/index.ts
Normal file
1
server/src/contexts/sales/domain/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./QuoteReferenceGeneratorService.interface";
|
||||||
@ -112,6 +112,28 @@ export class QuoteRepository extends SequelizeRepository<Quote> implements IQuot
|
|||||||
public async removeById(id: UniqueID, force: boolean = false): Promise<void> {
|
public async removeById(id: UniqueID, force: boolean = false): Promise<void> {
|
||||||
return this._removeById("Quote_Model", id);
|
return this._removeById("Quote_Model", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findLastQuoteByDealerId(dealerId: UniqueID): Promise<Quote | null> {
|
||||||
|
const Quote_Model: ModelDefined<any, any> = this._adapter.getModel("Quote_Model");
|
||||||
|
const QuoteItem_Model: ModelDefined<any, any> = this._adapter.getModel("QuoteItem_Model");
|
||||||
|
|
||||||
|
const rawQuote: any = await Quote_Model.findOne({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: QuoteItem_Model,
|
||||||
|
as: "items",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
where: { dealer_id: dealerId.toString() },
|
||||||
|
order: [["created_at", "DESC"]], // Ordenamos por referencia en orden descendente
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rawQuote === true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapper.mapToDomain(rawQuote);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const registerQuoteRepository = (context: ISalesContext) => {
|
export const registerQuoteRepository = (context: ISalesContext) => {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { IContext } from "@/contexts/common/infrastructure";
|
import { IContext } from "@/contexts/common/infrastructure";
|
||||||
import { Dealer } from "../domain";
|
import { Dealer, IQuoteReferenceGeneratorService } from "../domain";
|
||||||
|
|
||||||
export interface ISalesContext extends IContext {
|
export interface ISalesContext extends IContext {
|
||||||
//services: IApplicationService;
|
services: {
|
||||||
|
QuoteReferenceGeneratorService: IQuoteReferenceGeneratorService;
|
||||||
|
};
|
||||||
dealer?: Dealer;
|
dealer?: Dealer;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,16 +35,16 @@ export class CreateQuoteController extends ExpressController {
|
|||||||
async executeImpl() {
|
async executeImpl() {
|
||||||
try {
|
try {
|
||||||
const quoteDTO: ICreateQuote_Request_DTO = this.req.body;
|
const quoteDTO: ICreateQuote_Request_DTO = this.req.body;
|
||||||
/*const user = <User | undefined>this.req.user;
|
const dealer = this.context.dealer;
|
||||||
|
|
||||||
if (!user) {
|
if (!dealer) {
|
||||||
const errorMessage = "Unexpected missing user data";
|
const errorMessage = "Unexpected missing dealer data";
|
||||||
const infraError = InfrastructureError.create(
|
const infraError = InfrastructureError.create(
|
||||||
InfrastructureError.UNEXCEPTED_ERROR,
|
InfrastructureError.UNEXCEPTED_ERROR,
|
||||||
errorMessage
|
errorMessage
|
||||||
);
|
);
|
||||||
return this.internalServerError(errorMessage, infraError);
|
return this.internalServerError(errorMessage, infraError);
|
||||||
}*/
|
}
|
||||||
|
|
||||||
// Validaciones de DTO
|
// Validaciones de DTO
|
||||||
const quoteDTOOrError = ensureCreateQuote_Request_DTOIsValid(quoteDTO);
|
const quoteDTOOrError = ensureCreateQuote_Request_DTOIsValid(quoteDTO);
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const CreateQuotePresenter: ICreateQuotePresenter = {
|
|||||||
status: quote.status.toString(),
|
status: quote.status.toString(),
|
||||||
date: quote.date.toISO8601(),
|
date: quote.date.toISO8601(),
|
||||||
reference: quote.reference.toString(),
|
reference: quote.reference.toString(),
|
||||||
|
customer_reference: quote.customerReference.toString(),
|
||||||
customer_information: quote.customer.toString(),
|
customer_information: quote.customer.toString(),
|
||||||
lang_code: quote.language.toString(),
|
lang_code: quote.language.toString(),
|
||||||
currency_code: quote.currency.toString(),
|
currency_code: quote.currency.toString(),
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const GetQuotePresenter: IGetQuotePresenter = {
|
|||||||
status: quote.status.toString(),
|
status: quote.status.toString(),
|
||||||
date: quote.date.toISO8601(),
|
date: quote.date.toISO8601(),
|
||||||
reference: quote.reference.toString(),
|
reference: quote.reference.toString(),
|
||||||
|
customer_reference: quote.customerReference.toString(),
|
||||||
customer_information: quote.customer.toString(),
|
customer_information: quote.customer.toString(),
|
||||||
lang_code: quote.language.toString(),
|
lang_code: quote.language.toString(),
|
||||||
currency_code: quote.currency.toString(),
|
currency_code: quote.currency.toString(),
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export const ListQuotesPresenter: IListQuotesPresenter = {
|
|||||||
date: quote.date.toISO8601(),
|
date: quote.date.toISO8601(),
|
||||||
reference: quote.reference.toString(),
|
reference: quote.reference.toString(),
|
||||||
customer_information: quote.customer.toString(),
|
customer_information: quote.customer.toString(),
|
||||||
|
customer_reference: quote.customerReference.toString(),
|
||||||
lang_code: quote.language.toString(),
|
lang_code: quote.language.toString(),
|
||||||
currency_code: quote.currency.toString(),
|
currency_code: quote.currency.toString(),
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,7 @@ const map = (quote: Quote, context: ISalesContext) => {
|
|||||||
status: quote.status.toString(),
|
status: quote.status.toString(),
|
||||||
date: quote.date.toLocaleDateString(),
|
date: quote.date.toLocaleDateString(),
|
||||||
reference: quote.reference.toString(),
|
reference: quote.reference.toString(),
|
||||||
|
customer_reference: quote.customerReference.toString(),
|
||||||
customer_information: quote.customer.toString(),
|
customer_information: quote.customer.toString(),
|
||||||
lang_code: quote.language.toString(),
|
lang_code: quote.language.toString(),
|
||||||
currency_code: quote.currency.toString(),
|
currency_code: quote.currency.toString(),
|
||||||
|
|||||||
@ -1,466 +0,0 @@
|
|||||||
<html lang="es">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Presupuesto #29b6ac1a-ce83-44be-ac3a-714ceee1983f</title>
|
|
||||||
<style type="text/css">
|
|
||||||
.header {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para la impresión */
|
|
||||||
@media print {
|
|
||||||
.header {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
margin-top: 100px;
|
|
||||||
/* Ajusta según el tamaño de tu cabecera */
|
|
||||||
}
|
|
||||||
|
|
||||||
@page {
|
|
||||||
margin-top: 0;
|
|
||||||
/* Ajusta según el tamaño de tu cabecera */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="background-color: white;">
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-content">
|
|
||||||
<div class="dealer-info">
|
|
||||||
<h1>DISTRIBUIDOR OFICIAL</h1>
|
|
||||||
<div class="contact-info">
|
|
||||||
<div class="mt-2">
|
|
||||||
<img src="https://via.placeholder.com/100x50" alt="Logo distribuidor" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-sm">
|
|
||||||
GRUPO DE INTERIORES GTH, S.L.
|
|
||||||
Calle Castilla, 204
|
|
||||||
28009 Madrid
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<img src="https://via.placeholder.com/150x50" alt="Uecko Logo" />
|
|
||||||
<p class="text-xs text-gray-500">Essential Furniture</p>
|
|
||||||
<p class="text-xs text-gray-500">PREMIO AMBARRO DEL AÑO 2021</p>
|
|
||||||
<p class="text-xs text-gray-500">LUXURY SPAIN</p>
|
|
||||||
<p class="text-xs text-gray-500">ELLE 2021</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 pb-4 mb-4 border-b">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm"><strong>Presupuesto nº:</strong> 29b6ac1a-ce83-44be-ac3a-714ceee1983f</p>
|
|
||||||
<p class="text-sm"><strong>Fecha:</strong> 2024-07-24T00:00:00.000Z</p>
|
|
||||||
<p class="text-sm"><strong>Validez:</strong> 2024-02-05</p>
|
|
||||||
<p class="text-sm"><strong>Vendedor:</strong> GRUPO DE INTERIORES GTH, S.L.</p>
|
|
||||||
<p class="text-sm"><strong>Referencia cliente:</strong> PR/8633</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm">ALICIA PELAEZ SEVILLA, S.L.
|
|
||||||
AVDA. NAZARET - SEPARACION DE AMBIENTES Y PUERTAS DE PASO V2 SD</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h2 style="font-size: 1.25rem; font-weight: 600;">Cotización</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</header>
|
|
||||||
<footer>
|
|
||||||
<div class="page-footer">
|
|
||||||
<div class="page-footer-content">
|
|
||||||
<p>Información básica sobre protección de datos</p>
|
|
||||||
<p></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="page-header-space">HEADER
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<div class="page-content">
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<!-- Contenido de la página -->
|
|
||||||
</div>
|
|
||||||
<div class="page">
|
|
||||||
<table class="table-header">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="px-2 py-2 text-right border">Cant.</th>
|
|
||||||
<th class="px-2 py-2 border">Descripción</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Prec. Unitario</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Subtotal</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Dto (%)</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Importe total</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-body">
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="px-2 py-2 font-medium"> CORREDERAS</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P1
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x1500 mm. incluyendo Kit de casonetto. </td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">14,50 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">14,50 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">14,50 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P7
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x820 mm incluyendo Kit de casonetto.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P8
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x820 mm incluyendo Kit de casonetto.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P9
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x960 mm incluyendo Kit de casonetto.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,95 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,95 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,95 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P10
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x965 mm. incluyendo Kit de casonetto.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,95 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,95 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,95 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P12
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x760 mm. incluyendo Kit de casonetto.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P13
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x760 mm. incluyendo Kit de casonetto.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P14
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x747 mm. incluyendo Kit de casonetto.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">8,86 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P15
|
|
||||||
Puerta de paso corredera lisa lacada BLANCO AV-501 a determinar de dimensiones 2600x900 mm. incluyendo Kit de casonetto.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">9,60 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="px-2 py-2 font-medium"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="px-2 py-2 font-medium"> ABATIBLES</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P2
|
|
||||||
Puertas de paso abatible 1/c enrasada, sin cabecero, lisa lacado BLANCO AV-501 de 50 mm de espesor, de dimensiones de hoja 2600x860 mm. con cerco de 100/140x35, tapetas hasta 120x19 mm., resbalón unificado y bisagras ocultas negras. Apertura der</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P3
|
|
||||||
Puertas de paso abatible 1/c enrasada, sin cabecero, lisa lacado BLANCO AV-501 de 50 mm de espesor, de dimensiones de hoja 2600x885 mm. con cerco de 100/140x35, tapetas hasta 120x19 mm., resbalón unificado y bisagras ocultas negras. Apertura der</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1,75</td>
|
|
||||||
<td class="px-2 py-2 font-medium">Forrado paramentos 19 mm lacado m²</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">1,56 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">2,72 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">2,72 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P4
|
|
||||||
Puertas de paso abatible 1/c enrasada, sin cabecero, lisa lacado BLANCO AV-501 de 50 mm de espesor, de dimensiones de hoja 2600x985 mm. con cerco de 100/140x35, tapetas hasta 120x19 mm., resbalón unificado y bisagras ocultas negras. Apertura der</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,81 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,81 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,81 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P5
|
|
||||||
Puertas de paso abatible 1/c enrasada, sin cabecero, lisa lacado BLANCO AV-501 de 50 mm de espesor, de dimensiones de hoja 2600x885 mm. con cerco de 100/140x35, tapetas hasta 120x19 mm., resbalón unificado y bisagras ocultas negras. Apertura izq</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">1</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> P6
|
|
||||||
Puertas de paso abatible 1/c enrasada, sin cabecero, lisa lacado BLANCO AV-501 de 50 mm de espesor, de dimensiones de hoja 2600x885 mm. con cerco de 100/140x35, tapetas hasta 120x19 mm., resbalón unificado y bisagras ocultas negras. Apertura der</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,05 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="px-2 py-2 font-medium"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="px-2 py-2 font-medium"> NOTA.-
|
|
||||||
-Casonetto propiedad del cliente.
|
|
||||||
-Manivelas y/o condenas no incluidas.</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="px-2 py-2 font-medium"> DESCUENTO </td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="px-2 py-2 font-medium">null</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
<td class="content-start px-2 py-2 text-right"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">161</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> M.l. Rodapie lacado BLANCO AV-501 a determinar DE 200X19 mm con dos estrías horizontales </td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,15 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">24,79 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">24,79 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> DESCUENTO </td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="px-2 py-2 font-medium">null</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">3</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> Entrega de material en domicilio cliente</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">1,20 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">3,60 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">3,60 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> DESCUENTO </td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="px-2 py-2 font-medium">null</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">9</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> Instalación PUERTA DE PASO CORREDERA</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">1,30 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,70 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">11,70 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">5</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> Instalación PUERTA DE PASO ABATIBLE</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">1,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">5,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">5,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">159</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> Instalación M.L. RODAPIE </td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,09 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">13,52 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">13,52 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="px-2 py-2 font-medium"> DESCUENTO </td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="px-2 py-2 font-medium">null</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">0,00 €</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page"></div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between pb-4 mb-4 border-b">
|
|
||||||
<div>
|
|
||||||
<div class="pt-4 border-t">
|
|
||||||
<p class="text-sm"><strong>Forma de pago:</strong> 60% Aceptación presupuesto
|
|
||||||
30% Un día antes de la entrega del material
|
|
||||||
10% Restante a la finalización de la obra</p>
|
|
||||||
</div>
|
|
||||||
<div class="pt-4 border-t">
|
|
||||||
<p class="text-sm"><strong>Notas:</strong> Este presupuesto no se considerará en firme hasta confirmar medidas y diseños definitivos. </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<table class="table-footer">
|
|
||||||
<tbody>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">Importe neto</td>
|
|
||||||
<td class="px-4 py-2 border"></td>
|
|
||||||
<td class="px-4 py-2 text-right border">207,12 €</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">% Descuento</td>
|
|
||||||
<td class="px-4 py-2 text-right border"></td>
|
|
||||||
<td class="px-4 py-2 text-right border"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">Impuestos</td>
|
|
||||||
<td class="px-4 py-2 text-right border"></td>
|
|
||||||
<td class="px-4 py-2 text-right border"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">Envío</td>
|
|
||||||
<td class="px-4 py-2 text-right border"></td>
|
|
||||||
<td class="px-4 py-2 text-right border"></td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 font-medium border">Total</td>
|
|
||||||
<td class="px-4 py-2 font-medium border"></td>
|
|
||||||
<td class="px-4 py-2 font-medium text-right border">207,12 €</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@ -1,319 +0,0 @@
|
|||||||
<html lang="{{lang_code}}">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Presupuesto #{{id}}</title>
|
|
||||||
<style type="text/css">
|
|
||||||
@page {
|
|
||||||
font-family: Pacifico;
|
|
||||||
margin: 1cm;
|
|
||||||
|
|
||||||
@bottom-left {
|
|
||||||
color: #1ee494;
|
|
||||||
content: "♥ Thank you!";
|
|
||||||
}
|
|
||||||
|
|
||||||
@bottom-right {
|
|
||||||
color: #a9a;
|
|
||||||
content: "contact@courtbouillon.org | courtbouillon.org";
|
|
||||||
font-size: 9pt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullpage {
|
|
||||||
page: full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
margin: 7cm 0 0 0;
|
|
||||||
page-break-after: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
html {
|
|
||||||
color: #14213d;
|
|
||||||
font-family: Source Sans Pro;
|
|
||||||
font-size: 11pt;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
position: fixed;
|
|
||||||
top: 0mm;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 1px solid black;
|
|
||||||
/* for demo */
|
|
||||||
background: transparent;
|
|
||||||
/* for demo */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.page-header-space {}
|
|
||||||
|
|
||||||
.page-footer-space {}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #1ee494;
|
|
||||||
font-family: Pacifico;
|
|
||||||
font-size: 40pt;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside {
|
|
||||||
display: flex;
|
|
||||||
margin: 2em 0 4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside address {
|
|
||||||
flex: 1;
|
|
||||||
font-style: normal;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside address#from {
|
|
||||||
border: 1px solid red;
|
|
||||||
color: #a9a;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside address#to {
|
|
||||||
border: 1px solid green;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
text-align: right;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt,
|
|
||||||
dd {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt {
|
|
||||||
color: #a9a;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
dt::after {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
border-bottom: 0.2mm solid #a9a;
|
|
||||||
color: #a9a;
|
|
||||||
font-size: 10pt;
|
|
||||||
font-weight: 400;
|
|
||||||
padding-bottom: 0.25cm;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding-top: 7mm;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:last-of-type {
|
|
||||||
color: #1ee494;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
th:first-of-type,
|
|
||||||
td:first-of-type {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
th:last-of-type,
|
|
||||||
td:last-of-type {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
height: 6cm;
|
|
||||||
}
|
|
||||||
|
|
||||||
table#total {
|
|
||||||
background: #f6f6f6;
|
|
||||||
border-color: #f6f6f6;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 2cm 3cm;
|
|
||||||
bottom: 0;
|
|
||||||
font-size: 20pt;
|
|
||||||
margin: 0 -3cm;
|
|
||||||
position: absolute;
|
|
||||||
width: 18cm;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
thead {
|
|
||||||
display: table-header-group;
|
|
||||||
}
|
|
||||||
|
|
||||||
tfoot {
|
|
||||||
display: table-footer-group;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="page-header-space">
|
|
||||||
<header>
|
|
||||||
<h1>Invoice</h1>
|
|
||||||
|
|
||||||
<aside>
|
|
||||||
<address id="from">
|
|
||||||
{{dealer.contact_information}}
|
|
||||||
</address>
|
|
||||||
|
|
||||||
<address id="to">
|
|
||||||
{{customer_information}}
|
|
||||||
</address>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<dl id="informations">
|
|
||||||
</header>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tincidunt metus eu consectetur rutrum.
|
|
||||||
Praesent tempor facilisis dapibus. Aliquam cursus diam ac vehicula pulvinar. Integer lacinia non odio et
|
|
||||||
condimentum. Aenean faucibus cursus mi, sed interdum turpis sagittis a. Quisque quis pellentesque mi. Ut
|
|
||||||
erat eros, posuere sed scelerisque ut, pharetra vitae tellus. Suspendisse ligula sapien, laoreet ac
|
|
||||||
hendrerit sit amet, viverra vel mi. Pellentesque faucibus nisl et dolor pharetra, vel mattis massa
|
|
||||||
venenatis. Integer congue condimentum nisi, sed tincidunt velit tincidunt non. Nulla sagittis sed lorem
|
|
||||||
pretium aliquam. Praesent consectetur volutpat nibh, quis pulvinar est volutpat id. Cras maximus odio
|
|
||||||
posuere suscipit venenatis. Donec rhoncus scelerisque metus, in tempus erat rhoncus sed. Morbi massa
|
|
||||||
sapien,
|
|
||||||
porttitor id urna vel, volutpat blandit velit. Cras sit amet sem eros. Quisque commodo facilisis
|
|
||||||
tristique.
|
|
||||||
Proin pellentesque sodales rutrum. Vestibulum purus neque, congue vel dapibus in, venenatis ut felis.
|
|
||||||
Donec
|
|
||||||
et ligula enim. Sed sapien sapien, tincidunt vitae lectus quis, ultricies rhoncus mi. Nunc dapibus nulla
|
|
||||||
tempus nunc interdum, sed facilisis ex pellentesque. Nunc vel lorem leo. Cras pharetra sodales metus.
|
|
||||||
Cras
|
|
||||||
lacus ex, consequat at consequat vel, laoreet ac dui. Curabitur aliquam, sapien quis congue feugiat,
|
|
||||||
nisi
|
|
||||||
nisl feugiat diam, sed vehicula velit nulla ac nisl. Aliquam quis nisi euismod massa blandit pharetra
|
|
||||||
nec
|
|
||||||
eget nunc. Etiam eros ante, auctor sit amet quam vel, fringilla faucibus leo. Morbi a pulvinar nulla.
|
|
||||||
Praesent sed vulputate nisl. Orci varius natoque penatibus et magnis dis parturient montes, nascetur
|
|
||||||
ridiculus mus. Aenean commodo mollis iaculis. Maecenas consectetur enim vitae mollis venenatis. Ut
|
|
||||||
scelerisque pretium orci id laoreet. In sit amet pharetra diam. Vestibulum in molestie lorem. Nunc
|
|
||||||
gravida,
|
|
||||||
eros non consequat fermentum, ex orci vestibulum orci, non accumsan sem velit ac lectus. Vivamus
|
|
||||||
malesuada
|
|
||||||
lacus nec velit dignissim, ac fermentum nulla pretium. Aenean mi nisi, convallis sed tempor in,
|
|
||||||
porttitor
|
|
||||||
eu
|
|
||||||
libero. Praesent et molestie ante. Duis suscipit vitae purus sit amet aliquam. Vestibulum lectus justo,
|
|
||||||
lobortis a purus a, dapibus efficitur metus. Suspendisse potenti. Duis dictum ex lorem. Suspendisse nec
|
|
||||||
ligula consectetur magna hendrerit ullamcorper et eget mauris. Etiam vestibulum sodales diam, eget
|
|
||||||
venenatis
|
|
||||||
nunc luctus quis. Ut fermentum placerat neque nec elementum. Praesent orci erat, rhoncus vitae est eu,
|
|
||||||
dictum molestie metus. Cras et fermentum elit. Aenean eget augue lacinia, varius ante in, ullamcorper
|
|
||||||
dolor.
|
|
||||||
Cras viverra purus non egestas consectetur. Nulla nec dolor ac lectus convallis aliquet sed a metus.
|
|
||||||
Suspendisse eu imperdiet nunc, id pulvinar risus. Maecenas varius sagittis est, vel fermentum risus
|
|
||||||
accumsan
|
|
||||||
at. Vestibulum sollicitudin dui pharetra sapien volutpat, id convallis mi vestibulum. Phasellus commodo
|
|
||||||
sit
|
|
||||||
amet lorem quis imperdiet. Proin nec diam sed urna euismod ultricies at sed urna. Quisque ornare, nulla
|
|
||||||
et
|
|
||||||
vehicula ultrices, massa purus vehicula urna, ac sodales lacus leo vitae mi. Sed congue placerat justo
|
|
||||||
at
|
|
||||||
placerat. Aenean suscipit fringilla vehicula. Quisque iaculis orci vitae arcu commodo maximus. Maecenas
|
|
||||||
nec
|
|
||||||
nunc rutrum, cursus elit quis, porttitor sapien. Sed ac hendrerit ipsum, lacinia fringilla velit. Donec
|
|
||||||
ultricies feugiat dictum.
|
|
||||||
</div>
|
|
||||||
<div class="page">
|
|
||||||
<table class="min-w-full bg-white page">
|
|
||||||
<thead class="bg-gray-200">
|
|
||||||
<tr class="text-xs">
|
|
||||||
<th class="px-2 py-2 text-right border">Cant.</th>
|
|
||||||
<th class="px-2 py-2 border">Descripción</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Prec. Unitario</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Subtotal</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Dto (%)</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Importe total</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each items}}
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{quantity}}</td>
|
|
||||||
<td class="px-2 py-2 font-medium">{{description}}</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{unit_price}}</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{subtotal_price}}</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{discount}}</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{total_price}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<table id="total">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Due by</th>
|
|
||||||
<th>Account number</th>
|
|
||||||
<th>Total due</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>May 10, 2018</td>
|
|
||||||
<td>132 456 789 012</td>
|
|
||||||
<td>€10,545.00</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: Pacifico;
|
|
||||||
src: url(pacifico.ttf);
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: Source Sans Pro;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url(sourcesanspro-regular.otf);
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: Source Sans Pro;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url(sourcesanspro-bold.otf);
|
|
||||||
}
|
|
||||||
|
|
||||||
@page {
|
|
||||||
font-family: Pacifico;
|
|
||||||
margin: 3cm;
|
|
||||||
@bottom-left {
|
|
||||||
color: #1ee494;
|
|
||||||
content: "♥ Thank you!";
|
|
||||||
}
|
|
||||||
@bottom-right {
|
|
||||||
color: #a9a;
|
|
||||||
content: "contact@courtbouillon.org | courtbouillon.org";
|
|
||||||
font-size: 9pt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullpage {
|
|
||||||
page: full;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
color: #14213d;
|
|
||||||
font-family: Source Sans Pro;
|
|
||||||
font-size: 11pt;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #1ee494;
|
|
||||||
font-family: Pacifico;
|
|
||||||
font-size: 40pt;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside {
|
|
||||||
display: flex;
|
|
||||||
margin: 2em 0 4em;
|
|
||||||
}
|
|
||||||
aside address {
|
|
||||||
font-style: normal;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
aside address#from {
|
|
||||||
color: #a9a;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
aside address#to {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
text-align: right;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
dt,
|
|
||||||
dd {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
dt {
|
|
||||||
color: #a9a;
|
|
||||||
}
|
|
||||||
dt::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
dt::after {
|
|
||||||
content: ":";
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
border-bottom: 0.2mm solid #a9a;
|
|
||||||
color: #a9a;
|
|
||||||
font-size: 10pt;
|
|
||||||
font-weight: 400;
|
|
||||||
padding-bottom: 0.25cm;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding-top: 7mm;
|
|
||||||
}
|
|
||||||
td:last-of-type {
|
|
||||||
color: #1ee494;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
th:first-of-type,
|
|
||||||
td:first-of-type {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
th:last-of-type,
|
|
||||||
td:last-of-type {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
height: 6cm;
|
|
||||||
}
|
|
||||||
table#total {
|
|
||||||
background: #f6f6f6;
|
|
||||||
border-color: #f6f6f6;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 2cm 3cm;
|
|
||||||
bottom: 0;
|
|
||||||
font-size: 20pt;
|
|
||||||
margin: 0 -3cm;
|
|
||||||
position: absolute;
|
|
||||||
width: 18cm;
|
|
||||||
}
|
|
||||||
@ -1,267 +0,0 @@
|
|||||||
<html lang="{{lang_code}}">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Presupuesto #{{id}}</title>
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css"
|
|
||||||
referrerpolicy="no-referrer" />
|
|
||||||
<style type="text/css">
|
|
||||||
header,
|
|
||||||
.page-header,
|
|
||||||
.page-header-space {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer,
|
|
||||||
.page-footer,
|
|
||||||
.page-footer-space {
|
|
||||||
height: 50px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
footer,
|
|
||||||
.page-footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 1px solid black;
|
|
||||||
/* for demo */
|
|
||||||
background: transparent;
|
|
||||||
/* for demo */
|
|
||||||
}
|
|
||||||
|
|
||||||
header,
|
|
||||||
.page-header {
|
|
||||||
position: fixed;
|
|
||||||
top: 0mm;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom: 1px solid black;
|
|
||||||
/* for demo */
|
|
||||||
background: transparent;
|
|
||||||
/* for demo */
|
|
||||||
}
|
|
||||||
|
|
||||||
.page {
|
|
||||||
page-break-after: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
@page {
|
|
||||||
margin: 20mm
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
thead {
|
|
||||||
display: table-header-group;
|
|
||||||
}
|
|
||||||
|
|
||||||
tfoot {
|
|
||||||
display: table-footer-group;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="bg-white">
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="flex items-center justify-between pb-4 mb-4 border-b">
|
|
||||||
<div class="bg-red-400 border border-green-500">
|
|
||||||
<h1 class="text-2xl font-bold">DISTRIBUIDOR OFICIAL</h1>
|
|
||||||
<div class="flex space-x-4 bg-blue-500 border">
|
|
||||||
<div class="mt-2">
|
|
||||||
<img src="https://via.placeholder.com/100x50" alt="Logo distribuidor" />
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-sm">
|
|
||||||
{{dealer.contact_information}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<img src="https://via.placeholder.com/150x50" alt="Uecko Logo" />
|
|
||||||
<p class="text-xs text-gray-500">Essential Furniture</p>
|
|
||||||
<p class="text-xs text-gray-500">PREMIO AMBARRO DEL AÑO 2021</p>
|
|
||||||
<p class="text-xs text-gray-500">LUXURY SPAIN</p>
|
|
||||||
<p class="text-xs text-gray-500">ELLE 2021</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 pb-4 mb-4 border-b">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm"><strong>Presupuesto nº:</strong> {{id}}</p>
|
|
||||||
<p class="text-sm"><strong>Fecha:</strong> {{date}}</p>
|
|
||||||
<p class="text-sm"><strong>Validez:</strong> {{validity}}</p>
|
|
||||||
<p class="text-sm"><strong>Vendedor:</strong> {{dealer.name}}</p>
|
|
||||||
<p class="text-sm"><strong>Referencia cliente:</strong> {{reference}}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm">{{customer_information}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h2 class="mb-2 text-xl font-semibold">Cotización</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-footer">
|
|
||||||
<div class="pt-4 mt-4 border-t">
|
|
||||||
<p class="text-xs text-gray-500">Información básica sobre protección de datos</p>
|
|
||||||
<p class="text-xs text-gray-500">{{quote.default_legal_terms}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="page-header-space">
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<div class="page-content">
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tincidunt metus eu consectetur rutrum.
|
|
||||||
Praesent tempor facilisis dapibus. Aliquam cursus diam ac vehicula pulvinar. Integer lacinia non odio et
|
|
||||||
condimentum. Aenean faucibus cursus mi, sed interdum turpis sagittis a. Quisque quis pellentesque mi. Ut
|
|
||||||
erat eros, posuere sed scelerisque ut, pharetra vitae tellus. Suspendisse ligula sapien, laoreet ac
|
|
||||||
hendrerit sit amet, viverra vel mi. Pellentesque faucibus nisl et dolor pharetra, vel mattis massa
|
|
||||||
venenatis. Integer congue condimentum nisi, sed tincidunt velit tincidunt non. Nulla sagittis sed lorem
|
|
||||||
pretium aliquam. Praesent consectetur volutpat nibh, quis pulvinar est volutpat id. Cras maximus odio
|
|
||||||
posuere suscipit venenatis. Donec rhoncus scelerisque metus, in tempus erat rhoncus sed. Morbi massa
|
|
||||||
sapien,
|
|
||||||
porttitor id urna vel, volutpat blandit velit. Cras sit amet sem eros. Quisque commodo facilisis
|
|
||||||
tristique.
|
|
||||||
Proin pellentesque sodales rutrum. Vestibulum purus neque, congue vel dapibus in, venenatis ut felis.
|
|
||||||
Donec
|
|
||||||
et ligula enim. Sed sapien sapien, tincidunt vitae lectus quis, ultricies rhoncus mi. Nunc dapibus nulla
|
|
||||||
tempus nunc interdum, sed facilisis ex pellentesque. Nunc vel lorem leo. Cras pharetra sodales metus. Cras
|
|
||||||
lacus ex, consequat at consequat vel, laoreet ac dui. Curabitur aliquam, sapien quis congue feugiat, nisi
|
|
||||||
nisl feugiat diam, sed vehicula velit nulla ac nisl. Aliquam quis nisi euismod massa blandit pharetra nec
|
|
||||||
eget nunc. Etiam eros ante, auctor sit amet quam vel, fringilla faucibus leo. Morbi a pulvinar nulla.
|
|
||||||
Praesent sed vulputate nisl. Orci varius natoque penatibus et magnis dis parturient montes, nascetur
|
|
||||||
ridiculus mus. Aenean commodo mollis iaculis. Maecenas consectetur enim vitae mollis venenatis. Ut
|
|
||||||
scelerisque pretium orci id laoreet. In sit amet pharetra diam. Vestibulum in molestie lorem. Nunc
|
|
||||||
gravida,
|
|
||||||
eros non consequat fermentum, ex orci vestibulum orci, non accumsan sem velit ac lectus. Vivamus malesuada
|
|
||||||
lacus nec velit dignissim, ac fermentum nulla pretium. Aenean mi nisi, convallis sed tempor in, porttitor
|
|
||||||
eu
|
|
||||||
libero. Praesent et molestie ante. Duis suscipit vitae purus sit amet aliquam. Vestibulum lectus justo,
|
|
||||||
lobortis a purus a, dapibus efficitur metus. Suspendisse potenti. Duis dictum ex lorem. Suspendisse nec
|
|
||||||
ligula consectetur magna hendrerit ullamcorper et eget mauris. Etiam vestibulum sodales diam, eget
|
|
||||||
venenatis
|
|
||||||
nunc luctus quis. Ut fermentum placerat neque nec elementum. Praesent orci erat, rhoncus vitae est eu,
|
|
||||||
dictum molestie metus. Cras et fermentum elit. Aenean eget augue lacinia, varius ante in, ullamcorper
|
|
||||||
dolor.
|
|
||||||
Cras viverra purus non egestas consectetur. Nulla nec dolor ac lectus convallis aliquet sed a metus.
|
|
||||||
Suspendisse eu imperdiet nunc, id pulvinar risus. Maecenas varius sagittis est, vel fermentum risus
|
|
||||||
accumsan
|
|
||||||
at. Vestibulum sollicitudin dui pharetra sapien volutpat, id convallis mi vestibulum. Phasellus commodo
|
|
||||||
sit
|
|
||||||
amet lorem quis imperdiet. Proin nec diam sed urna euismod ultricies at sed urna. Quisque ornare, nulla et
|
|
||||||
vehicula ultrices, massa purus vehicula urna, ac sodales lacus leo vitae mi. Sed congue placerat justo at
|
|
||||||
placerat. Aenean suscipit fringilla vehicula. Quisque iaculis orci vitae arcu commodo maximus. Maecenas
|
|
||||||
nec
|
|
||||||
nunc rutrum, cursus elit quis, porttitor sapien. Sed ac hendrerit ipsum, lacinia fringilla velit. Donec
|
|
||||||
ultricies feugiat dictum.
|
|
||||||
</div>
|
|
||||||
<div class="page">
|
|
||||||
<table class="min-w-full bg-white page">
|
|
||||||
<thead class="bg-gray-200">
|
|
||||||
<tr class="text-xs">
|
|
||||||
<th class="px-2 py-2 text-right border">Cant.</th>
|
|
||||||
<th class="px-2 py-2 border">Descripción</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Prec. Unitario</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Subtotal</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Dto (%)</th>
|
|
||||||
<th class="px-2 py-2 text-right border">Importe total</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each items}}
|
|
||||||
<tr class="text-sm border-b">
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{quantity}}</td>
|
|
||||||
<td class="px-2 py-2 font-medium">{{description}}</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{unit_price}}</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{subtotal_price}}</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{discount}}</td>
|
|
||||||
<td class="content-start px-2 py-2 text-right">{{total_price}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="page"></div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between pb-4 mb-4 border-b">
|
|
||||||
<div>
|
|
||||||
<div class="pt-4 border-t">
|
|
||||||
<p class="text-sm"><strong>Forma de pago:</strong> {{payment_method}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="pt-4 border-t">
|
|
||||||
<p class="text-sm"><strong>Notas:</strong> {{notes}} </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<table class="min-w-full bg-transparent">
|
|
||||||
<tbody>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">Importe neto</td>
|
|
||||||
<td class="px-4 py-2 border"></td>
|
|
||||||
<td class="px-4 py-2 text-right border">{{subtotal_price}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">% Descuento</td>
|
|
||||||
<td class="px-4 py-2 text-right border">{{discount.amount}}</td>
|
|
||||||
<td class="px-4 py-2 text-right border">{{discount_price}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">Base imponible</td>
|
|
||||||
<td class="px-4 py-2 border"></td>
|
|
||||||
<td class="px-4 py-2 text-right border">{{before_tax_price}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">% IVA</td>
|
|
||||||
<td class="px-4 py-2 text-right border">{{tax}}</td>
|
|
||||||
<td class="px-4 py-2 text-right border">{{tax_price}}</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b">
|
|
||||||
<td class="px-4 py-2 border">Importe total</td>
|
|
||||||
<td class="px-4 py-2 border"></td>
|
|
||||||
<td class="px-4 py-2 text-right border">{{total_price}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="page-footer-space"></div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@ -62,11 +62,11 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="grid grid-cols-2 gap-4 pb-4 mb-4 border-b">
|
<section class="grid grid-cols-2 gap-4 pb-4 mb-4 border-b">
|
||||||
<aside>
|
<aside>
|
||||||
<p class="text-sm"><strong>Presupuesto nº:</strong> {{id}}</p>
|
<p class="text-sm"><strong>Presupuesto nº:</strong> {{reference}}</p>
|
||||||
<p class="text-sm"><strong>Fecha:</strong> {{date}}</p>
|
<p class="text-sm"><strong>Fecha:</strong> {{date}}</p>
|
||||||
<p class="text-sm"><strong>Validez:</strong> {{validity}}</p>
|
<p class="text-sm"><strong>Validez:</strong> {{validity}}</p>
|
||||||
<p class="text-sm"><strong>Vendedor:</strong> {{dealer.name}}</p>
|
<p class="text-sm"><strong>Vendedor:</strong> {{dealer.name}}</p>
|
||||||
<p class="text-sm"><strong>Referencia cliente:</strong> {{reference}}</p>
|
<p class="text-sm"><strong>Referencia cliente:</strong> {{customer_reference}}</p>
|
||||||
</aside>
|
</aside>
|
||||||
<address class="text-base not-italic font-semibold whitespace-pre-line" id="to">{{customer_information}}
|
<address class="text-base not-italic font-semibold whitespace-pre-line" id="to">{{customer_information}}
|
||||||
</address>
|
</address>
|
||||||
|
|||||||
@ -9,7 +9,13 @@ import {
|
|||||||
} from "@shared/contexts";
|
} from "@shared/contexts";
|
||||||
|
|
||||||
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
||||||
import { IQuoteProps, Quote, QuoteCustomer, QuoteReference } from "../../domain";
|
import {
|
||||||
|
IQuoteProps,
|
||||||
|
Quote,
|
||||||
|
QuoteCustomer,
|
||||||
|
QuoteCustomerReference,
|
||||||
|
QuoteReference,
|
||||||
|
} from "../../domain";
|
||||||
import { QuoteStatus } from "../../domain/entities/Quotes/QuoteStatus";
|
import { QuoteStatus } from "../../domain/entities/Quotes/QuoteStatus";
|
||||||
import { ISalesContext } from "../Sales.context";
|
import { ISalesContext } from "../Sales.context";
|
||||||
import { QuoteCreationAttributes, Quote_Model } from "../sequelize";
|
import { QuoteCreationAttributes, Quote_Model } from "../sequelize";
|
||||||
@ -45,6 +51,11 @@ class QuoteMapper
|
|||||||
reference: this.mapsValue(source, "reference", QuoteReference.create),
|
reference: this.mapsValue(source, "reference", QuoteReference.create),
|
||||||
currency: this.mapsValue(source, "currency_code", CurrencyData.createFromCode),
|
currency: this.mapsValue(source, "currency_code", CurrencyData.createFromCode),
|
||||||
language: this.mapsValue(source, "lang_code", Language.createFromCode),
|
language: this.mapsValue(source, "lang_code", Language.createFromCode),
|
||||||
|
customerReference: this.mapsValue(
|
||||||
|
source,
|
||||||
|
"customer_reference",
|
||||||
|
QuoteCustomerReference.create
|
||||||
|
),
|
||||||
customer: this.mapsValue(source, "customer_information", QuoteCustomer.create),
|
customer: this.mapsValue(source, "customer_information", QuoteCustomer.create),
|
||||||
|
|
||||||
validity: this.mapsValue(source, "validity", Note.create),
|
validity: this.mapsValue(source, "validity", Note.create),
|
||||||
@ -108,6 +119,7 @@ class QuoteMapper
|
|||||||
reference: source.reference.toPrimitive(),
|
reference: source.reference.toPrimitive(),
|
||||||
currency_code: source.currency.toPrimitive(),
|
currency_code: source.currency.toPrimitive(),
|
||||||
lang_code: source.language.toPrimitive(),
|
lang_code: source.language.toPrimitive(),
|
||||||
|
customer_reference: source.customerReference.toPrimitive(),
|
||||||
customer_information: source.customer.toPrimitive(),
|
customer_information: source.customer.toPrimitive(),
|
||||||
validity: source.validity.toPrimitive(),
|
validity: source.validity.toPrimitive(),
|
||||||
payment_method: source.paymentMethod.toPrimitive(),
|
payment_method: source.paymentMethod.toPrimitive(),
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export class Quote_Model extends Model<
|
|||||||
declare date: CreationOptional<string>;
|
declare date: CreationOptional<string>;
|
||||||
declare reference: CreationOptional<string>;
|
declare reference: CreationOptional<string>;
|
||||||
declare lang_code: CreationOptional<string>;
|
declare lang_code: CreationOptional<string>;
|
||||||
|
declare customer_reference: CreationOptional<string>;
|
||||||
declare customer_information: CreationOptional<string>;
|
declare customer_information: CreationOptional<string>;
|
||||||
declare currency_code: CreationOptional<string>;
|
declare currency_code: CreationOptional<string>;
|
||||||
declare payment_method: CreationOptional<string>;
|
declare payment_method: CreationOptional<string>;
|
||||||
@ -100,6 +101,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
defaultValue: "EUR",
|
defaultValue: "EUR",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
customer_reference: {
|
||||||
|
type: new DataTypes.STRING(),
|
||||||
|
},
|
||||||
|
|
||||||
customer_information: {
|
customer_information: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
},
|
},
|
||||||
@ -179,6 +184,9 @@ export default (sequelize: Sequelize) => {
|
|||||||
reference: {
|
reference: {
|
||||||
[Op.like]: `%${value}%`,
|
[Op.like]: `%${value}%`,
|
||||||
},
|
},
|
||||||
|
customer_reference: {
|
||||||
|
[Op.like]: `%${value}%`,
|
||||||
|
},
|
||||||
customer_information: {
|
customer_information: {
|
||||||
[Op.like]: `%${value}%`,
|
[Op.like]: `%${value}%`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -40,4 +40,22 @@ export class Name extends StringValueObject {
|
|||||||
|
|
||||||
return Result.ok(new Name(validationResult.object));
|
return Result.ok(new Name(validationResult.object));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static generateAcronym(name: string): string {
|
||||||
|
const words = name.split(" ").map((word) => word[0].toUpperCase());
|
||||||
|
let acronym = words.join("");
|
||||||
|
|
||||||
|
// Asegurarse de que tenga 4 caracteres, recortando o añadiendo letras
|
||||||
|
if (acronym.length > 4) {
|
||||||
|
acronym = acronym.slice(0, 4);
|
||||||
|
} else if (acronym.length < 4) {
|
||||||
|
acronym = acronym.padEnd(4, "X"); // Se completa con 'X' si es necesario
|
||||||
|
}
|
||||||
|
|
||||||
|
return acronym;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAcronym(): string {
|
||||||
|
return Name.generateAcronym(this.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export interface ICreateQuote_Request_DTO {
|
|||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
reference: string;
|
customer_reference: string;
|
||||||
customer_information: string;
|
customer_information: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
@ -45,6 +45,7 @@ export function ensureCreateQuote_Request_DTOIsValid(quoteDTO: ICreateQuote_Requ
|
|||||||
status: Joi.string(),
|
status: Joi.string(),
|
||||||
date: Joi.string(),
|
date: Joi.string(),
|
||||||
reference: Joi.string(),
|
reference: Joi.string(),
|
||||||
|
customer_reference: Joi.string(),
|
||||||
customer_information: Joi.string(),
|
customer_information: Joi.string(),
|
||||||
}).unknown(true);
|
}).unknown(true);
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface ICreateQuote_Response_DTO {
|
|||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
customer_reference: string;
|
||||||
customer_information: string;
|
customer_information: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface IGetQuote_Response_DTO {
|
|||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
customer_reference: string;
|
||||||
customer_information: string;
|
customer_information: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface IListQuotes_Response_DTO {
|
|||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
customer_reference: string;
|
||||||
customer_information: string;
|
customer_information: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
export interface IReportQuote_Response_DTO {
|
|
||||||
data: Uint8Array;
|
|
||||||
original: ArrayBuffer;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./IReportQuote_Response.dto";
|
|
||||||
@ -11,6 +11,7 @@ export interface IUpdateQuote_Request_DTO {
|
|||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
customer_reference: string;
|
||||||
customer_information: string;
|
customer_information: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
@ -43,6 +44,7 @@ export function ensureUpdateQuote_Request_DTOIsValid(quoteDTO: IUpdateQuote_Requ
|
|||||||
status: Joi.string(),
|
status: Joi.string(),
|
||||||
date: Joi.string(),
|
date: Joi.string(),
|
||||||
reference: Joi.string(),
|
reference: Joi.string(),
|
||||||
|
customer_reference: Joi.string(),
|
||||||
customer_information: Joi.string(),
|
customer_information: Joi.string(),
|
||||||
lang_code: Joi.string(),
|
lang_code: Joi.string(),
|
||||||
currency_code: Joi.string(),
|
currency_code: Joi.string(),
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface IUpdateQuote_Response_DTO {
|
|||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
customer_reference: string;
|
||||||
customer_information: string;
|
customer_information: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./CreateQuote.dto";
|
export * from "./CreateQuote.dto";
|
||||||
export * from "./GetQuote.dto";
|
export * from "./GetQuote.dto";
|
||||||
export * from "./ListQuotes.dto";
|
export * from "./ListQuotes.dto";
|
||||||
export * from "./ReportQuote.dto";
|
|
||||||
export * from "./UpdateQuote.dto";
|
export * from "./UpdateQuote.dto";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user