2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 19:09:01 +00:00

Merge pull request #788 from frappe/mildred/151-quotations

feat: #151 Quotations
This commit is contained in:
Mildred Ki'Lya 2024-01-08 22:05:13 +01:00 committed by GitHub
commit bcd0cc8d61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 321 additions and 20 deletions

View File

@ -21,6 +21,7 @@ const defaultNumberSeriesMap = {
[ModelNameEnum.JournalEntry]: 'JV-',
[ModelNameEnum.SalesInvoice]: 'SINV-',
[ModelNameEnum.PurchaseInvoice]: 'PINV-',
[ModelNameEnum.SalesQuote]: 'SQUOT-',
} as Record<ModelNameEnum, string>;
async function execute(dm: DatabaseManager) {
@ -209,6 +210,7 @@ async function copyTransactionalTables(
ModelNameEnum.Payment,
ModelNameEnum.SalesInvoice,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.SalesQuote,
];
for (const sn of schemaNames) {

View File

@ -14,6 +14,7 @@ export class Defaults extends Doc {
purchaseReceiptLocation?: string;
// Number Series
salesQuoteNumberSeries?: string;
salesInvoiceNumberSeries?: string;
purchaseInvoiceNumberSeries?: string;
journalEntryNumberSeries?: string;
@ -29,6 +30,7 @@ export class Defaults extends Doc {
purchaseReceiptTerms?: string;
// Print Templates
salesQuotePrintTemplate?: string;
salesInvoicePrintTemplate?: string;
purchaseInvoicePrintTemplate?: string;
journalEntryPrintTemplate?: string;
@ -46,6 +48,9 @@ export class Defaults extends Doc {
salesPaymentAccount: () => ({ isGroup: false, accountType: 'Cash' }),
purchasePaymentAccount: () => ({ isGroup: false, accountType: 'Cash' }),
// Number Series
salesQuoteNumberSeries: () => ({
referenceType: ModelNameEnum.SalesQuote,
}),
salesInvoiceNumberSeries: () => ({
referenceType: ModelNameEnum.SalesInvoice,
}),
@ -68,6 +73,7 @@ export class Defaults extends Doc {
referenceType: ModelNameEnum.PurchaseReceipt,
}),
// Print Templates
salesQuotePrintTemplate: () => ({ type: ModelNameEnum.SalesQuote }),
salesInvoicePrintTemplate: () => ({ type: ModelNameEnum.SalesInvoice }),
purchaseInvoicePrintTemplate: () => ({
type: ModelNameEnum.PurchaseInvoice,
@ -118,4 +124,5 @@ export const numberSeriesDefaultsMap: Record<
[ModelNameEnum.StockMovement]: 'stockMovementNumberSeries',
[ModelNameEnum.Shipment]: 'shipmentNumberSeries',
[ModelNameEnum.PurchaseReceipt]: 'purchaseReceiptNumberSeries',
[ModelNameEnum.SalesQuote]: 'salesQuoteNumberSeries',
};

View File

@ -71,7 +71,13 @@ export abstract class Invoice extends Transactional {
returnAgainst?: string;
get isSales() {
return this.schemaName === 'SalesInvoice';
return (
this.schemaName === 'SalesInvoice' || this.schemaName == 'SalesQuote'
);
}
get isQuote() {
return this.schemaName == 'SalesQuote';
}
get enableDiscounting() {
@ -493,7 +499,7 @@ export abstract class Invoice extends Transactional {
}
async _updateIsItemsReturned() {
if (!this.isReturn || !this.returnAgainst) {
if (!this.isReturn || !this.returnAgainst || this.isQuote) {
return;
}
@ -515,7 +521,7 @@ export abstract class Invoice extends Transactional {
}
async _validateHasLinkedReturnInvoices() {
if (!this.name || this.isReturn) {
if (!this.name || this.isReturn || this.isQuote) {
return;
}
@ -685,6 +691,7 @@ export abstract class Invoice extends Transactional {
attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
backReference: () => !this.backReference,
quote: () => !this.quote,
priceList: () => !this.fyo.singles.AccountingSettings?.enablePriceList,
returnAgainst: () =>
(this.isSubmitted || this.isCancelled) && !this.returnAgainst,

View File

@ -47,7 +47,10 @@ export abstract class InvoiceItem extends Doc {
itemTaxedTotal?: Money;
get isSales() {
return this.schemaName === 'SalesInvoiceItem';
return (
this.schemaName === 'SalesInvoiceItem' ||
this.schemaName === 'SalesQuoteItem'
);
}
get date() {

View File

@ -55,6 +55,7 @@ export class PrintTemplate extends Doc {
const models = [
ModelNameEnum.SalesInvoice,
ModelNameEnum.SalesQuote,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.JournalEntry,
ModelNameEnum.Payment,

View File

@ -0,0 +1,67 @@
import { Fyo } from 'fyo';
import { DocValueMap } from 'fyo/core/types';
import { Action, ListViewSettings } from 'fyo/model/types';
import { ModelNameEnum } from 'models/types';
import { getQuoteActions, getTransactionStatusColumn } from '../../helpers';
import { Invoice } from '../Invoice/Invoice';
import { SalesQuoteItem } from '../SalesQuoteItem/SalesQuoteItem';
import { Defaults } from '../Defaults/Defaults';
export class SalesQuote extends Invoice {
items?: SalesQuoteItem[];
// This is an inherited method and it must keep the async from the parent
// class
// eslint-disable-next-line @typescript-eslint/require-await
async getPosting() {
return null;
}
async getInvoice(): Promise<Invoice | null> {
if (!this.isSubmitted) {
return null;
}
const schemaName = ModelNameEnum.SalesInvoice;
const defaults = (this.fyo.singles.Defaults as Defaults) ?? {};
const terms = defaults.salesInvoiceTerms ?? '';
const numberSeries = defaults.salesInvoiceNumberSeries ?? undefined;
const data: DocValueMap = {
...this.getValidDict(false, true),
date: new Date().toISOString(),
terms,
numberSeries,
quote: this.name,
items: [],
};
const invoice = this.fyo.doc.getNewDoc(schemaName, data) as Invoice;
for (const row of this.items ?? []) {
await invoice.append('items', row.getValidDict(false, true));
}
if (!invoice.items?.length) {
return null;
}
return invoice;
}
static getListViewSettings(): ListViewSettings {
return {
columns: [
'name',
getTransactionStatusColumn(),
'party',
'date',
'baseGrandTotal',
'outstandingAmount',
],
};
}
static getActions(fyo: Fyo): Action[] {
return getQuoteActions(fyo, ModelNameEnum.SalesQuote);
}
}

View File

@ -0,0 +1,3 @@
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
export class SalesQuoteItem extends InvoiceItem {}

View File

@ -11,10 +11,18 @@ import {
} from './baseModels/Account/types';
import { numberSeriesDefaultsMap } from './baseModels/Defaults/Defaults';
import { Invoice } from './baseModels/Invoice/Invoice';
import { SalesQuote } from './baseModels/SalesQuote/SalesQuote';
import { StockMovement } from './inventory/StockMovement';
import { StockTransfer } from './inventory/StockTransfer';
import { InvoiceStatus, ModelNameEnum } from './types';
export function getQuoteActions(
fyo: Fyo,
schemaName: ModelNameEnum.SalesQuote
): Action[] {
return [getMakeInvoiceAction(fyo, schemaName)];
}
export function getInvoiceActions(
fyo: Fyo,
schemaName: ModelNameEnum.SalesInvoice | ModelNameEnum.PurchaseInvoice
@ -67,7 +75,10 @@ export function getMakeStockTransferAction(
export function getMakeInvoiceAction(
fyo: Fyo,
schemaName: ModelNameEnum.Shipment | ModelNameEnum.PurchaseReceipt
schemaName:
| ModelNameEnum.Shipment
| ModelNameEnum.PurchaseReceipt
| ModelNameEnum.SalesQuote
): Action {
let label = fyo.t`Sales Invoice`;
if (schemaName === ModelNameEnum.PurchaseReceipt) {
@ -77,9 +88,15 @@ export function getMakeInvoiceAction(
return {
label,
group: fyo.t`Create`,
condition: (doc: Doc) => doc.isSubmitted && !doc.backReference,
condition: (doc: Doc) => {
if (schemaName === ModelNameEnum.SalesQuote) {
return doc.isSubmitted;
} else {
return doc.isSubmitted && !doc.backReference;
}
},
action: async (doc: Doc) => {
const invoice = await (doc as StockTransfer).getInvoice();
const invoice = await (doc as SalesQuote | StockTransfer).getInvoice();
if (!invoice || !invoice.name) {
return;
}

View File

@ -19,6 +19,8 @@ import { PurchaseInvoice } from './baseModels/PurchaseInvoice/PurchaseInvoice';
import { PurchaseInvoiceItem } from './baseModels/PurchaseInvoiceItem/PurchaseInvoiceItem';
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
import { SalesInvoiceItem } from './baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { SalesQuote } from './baseModels/SalesQuote/SalesQuote';
import { SalesQuoteItem } from './baseModels/SalesQuoteItem/SalesQuoteItem';
import { SetupWizard } from './baseModels/SetupWizard/SetupWizard';
import { Tax } from './baseModels/Tax/Tax';
import { TaxSummary } from './baseModels/TaxSummary/TaxSummary';
@ -61,6 +63,8 @@ export const models = {
PurchaseInvoiceItem,
SalesInvoice,
SalesInvoiceItem,
SalesQuote,
SalesQuoteItem,
SerialNumber,
SetupWizard,
PrintTemplate,

View File

@ -27,6 +27,8 @@ export enum ModelNameEnum {
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
SalesInvoice = 'SalesInvoice',
SalesInvoiceItem = 'SalesInvoiceItem',
SalesQuote = 'SalesQuote',
SalesQuoteItem = 'SalesQuoteItem',
SerialNumber = 'SerialNumber',
SetupWizard = 'SetupWizard',
Tax = 'Tax',

View File

@ -92,6 +92,14 @@
"create": true,
"section": "Number Series"
},
{
"fieldname": "salesQuoteNumberSeries",
"label": "Sales Quote Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"section": "Number Series"
},
{
"fieldname": "salesInvoiceTerms",
"label": "Sales Invoice Terms",
@ -116,6 +124,13 @@
"fieldtype": "Text",
"section": "Terms"
},
{
"fieldname": "salesQuotePrintTemplate",
"label": "Sales Quote Print Template",
"fieldtype": "Link",
"target": "PrintTemplate",
"section": "Print Templates"
},
{
"fieldname": "salesInvoicePrintTemplate",
"label": "Sales Invoice Print Template",

View File

@ -35,6 +35,10 @@
"value": "SalesInvoice",
"label": "Sales Invoice"
},
{
"value": "SalesQuote",
"label": "Sales Quote"
},
{
"value": "PurchaseInvoice",
"label": "Purchase Invoice"

View File

@ -31,6 +31,14 @@
"target": "Shipment",
"section": "References"
},
{
"fieldname": "quote",
"label": "Quote Reference",
"fieldtype": "Link",
"target": "SalesQuote",
"section": "References",
"required": false
},
{
"fieldname": "makeAutoStockTransfer",
"label": "Make Shipment On Submit",

View File

@ -0,0 +1,46 @@
{
"name": "SalesQuote",
"label": "Quote",
"extends": "Invoice",
"naming": "numberSeries",
"showTitle": true,
"fields": [
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "SQUOT-",
"section": "Default"
},
{
"fieldname": "party",
"label": "Customer",
"fieldtype": "Link",
"target": "Party",
"create": true,
"required": true,
"section": "Default"
},
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"target": "SalesQuoteItem",
"required": true,
"edit": true,
"section": "Items"
}
],
"keywordFields": ["name", "party"],
"removeFields": [
"account",
"stockNotTransferred",
"backReference",
"makeAutoStockTransfer",
"returnAgainst",
"isReturned"
]
}

View File

@ -0,0 +1,5 @@
{
"name": "SalesQuoteItem",
"label": "Sales Quote Item",
"extends": "InvoiceItem"
}

View File

@ -25,6 +25,8 @@ import PurchaseInvoice from './app/PurchaseInvoice.json';
import PurchaseInvoiceItem from './app/PurchaseInvoiceItem.json';
import SalesInvoice from './app/SalesInvoice.json';
import SalesInvoiceItem from './app/SalesInvoiceItem.json';
import SalesQuote from './app/SalesQuote.json';
import SalesQuoteItem from './app/SalesQuoteItem.json';
import SetupWizard from './app/SetupWizard.json';
import Tax from './app/Tax.json';
import TaxDetail from './app/TaxDetail.json';
@ -108,10 +110,12 @@ export const appSchemas: Schema[] | SchemaStub[] = [
Invoice as Schema,
SalesInvoice as Schema,
PurchaseInvoice as Schema,
SalesQuote as Schema,
InvoiceItem as Schema,
SalesInvoiceItem as SchemaStub,
PurchaseInvoiceItem as SchemaStub,
SalesQuoteItem as SchemaStub,
PriceList as Schema,
PriceListItem as SchemaStub,

View File

@ -150,6 +150,7 @@
v-if="showDevMode"
class="text-xs text-gray-500 select-none cursor-pointer"
@click="showDevMode = false"
title="Open dev tools with Ctrl+Shift+I"
>
dev mode
</p>

View File

@ -0,0 +1,67 @@
<template>
<div class="w-form">
<FormHeader :form-title="t`Set Print Size`" />
<hr />
<div class="p-4 w-full flex flex-col gap-4">
<p class="text-base text-gray-900">
{{ t`Select the template type.` }}
</p>
<Select
:df="df"
:value="type"
:border="true"
:show-label="true"
@change="typeChange"
/>
</div>
<div class="flex border-t p-4">
<Button class="ml-auto" type="primary" @click="done">{{
t`Done`
}}</Button>
</div>
</div>
</template>
<script lang="ts">
import { PrintTemplate } from 'models/baseModels/PrintTemplate';
import { OptionField } from 'schemas/types';
import Button from 'src/components/Button.vue';
import Select from 'src/components/Controls/Select.vue';
import FormHeader from 'src/components/FormHeader.vue';
import { defineComponent } from 'vue';
export default defineComponent({
components: { FormHeader, Select, Button },
props: { doc: { type: PrintTemplate, required: true } },
emits: ['done'],
data() {
return { type: 'SalesInvoice' };
},
computed: {
df(): OptionField {
const options = PrintTemplate.lists.type(this.doc);
return {
...fyo.getField('PrintTemplate', 'type'),
options,
fieldtype: 'Select',
default: options[0].value,
} as OptionField;
},
},
mounted() {
this.type = this.doc.type ?? 'SalesInvoice';
},
methods: {
typeChange(v: string) {
if (this.type === v) {
return;
}
this.type = v;
},
async done() {
await this.doc.set('type', this.type);
this.$emit('done');
},
},
});
</script>

View File

@ -213,6 +213,13 @@
>
<SetPrintSize :doc="doc" @done="showSizeModal = !showSizeModal" />
</Modal>
<Modal
v-if="doc"
:open-modal="showTypeModal"
@closemodal="showTypeModal = !showTypeModal"
>
<SetType :doc="doc" @done="showTypeModal = !showTypeModal" />
</Modal>
</div>
</template>
<script lang="ts">
@ -256,6 +263,7 @@ import { getMapFromList } from 'utils/index';
import { computed, defineComponent, inject, ref } from 'vue';
import PrintContainer from './PrintContainer.vue';
import SetPrintSize from './SetPrintSize.vue';
import SetType from './SetType.vue';
import TemplateBuilderHint from './TemplateBuilderHint.vue';
import TemplateEditor from './TemplateEditor.vue';
@ -273,6 +281,7 @@ export default defineComponent({
Link,
Modal,
SetPrintSize,
SetType,
},
provide() {
return { doc: computed(() => this.doc) };
@ -303,6 +312,7 @@ export default defineComponent({
scale: 0.6,
panelWidth: 22 /** rem */ * 16 /** px */,
templateChanged: false,
showTypeModal: false,
showSizeModal: false,
preEditMode: {
scale: 0.6,
@ -315,6 +325,7 @@ export default defineComponent({
hints?: PrintTemplateHint;
values: null | PrintValues;
displayDoc: PrintTemplate | null;
showTypeModal: boolean;
showSizeModal: boolean;
scale: number;
panelWidth: number;
@ -367,6 +378,14 @@ export default defineComponent({
},
});
if (this.doc.isCustom && !this.showTypeModal) {
actions.push({
label: this.t`Set Template Type`,
group: this.t`Action`,
action: () => (this.showTypeModal = true),
});
}
if (this.doc.isCustom && !this.showSizeModal) {
actions.push({
label: this.t`Set Print Size`,

View File

@ -37,8 +37,6 @@ export async function getPrintTemplatePropValues(
const fyo = doc.fyo;
const values: PrintValues = { doc: {}, print: {} };
values.doc = await getPrintTemplateDocValues(doc);
(values.doc as PrintTemplateData).entryType = doc.schema.name;
(values.doc as PrintTemplateData).entryLabel = doc.schema.label;
const printSettings = await fyo.doc.getDoc(ModelNameEnum.PrintSettings);
const printValues = await getPrintTemplateDocValues(
@ -72,8 +70,6 @@ export function getPrintTemplatePropHints(schemaName: string, fyo: Fyo) {
const hints: PrintTemplateHint = {};
const schema = fyo.schemaMap[schemaName]!;
hints.doc = getPrintTemplateDocHints(schema, fyo);
hints.doc.entryType = fyo.t`Entry Type`;
hints.doc.entryLabel = fyo.t`Entry Label`;
const printSettingsHints = getPrintTemplateDocHints(
fyo.schemaMap[ModelNameEnum.PrintSettings]!,
@ -159,6 +155,10 @@ function getPrintTemplateDocHints(
}
}
hints.submitted = fyo.t`Submitted`;
hints.entryType = fyo.t`Entry Type`;
hints.entryLabel = fyo.t`Entry Label`;
if (Object.keys(links).length) {
hints.links = links;
}
@ -204,6 +204,10 @@ async function getPrintTemplateDocValues(doc: Doc, fieldnames?: string[]) {
values[fieldname] = table;
}
values.submitted = doc.submitted;
values.entryType = doc.schema.name;
values.entryLabel = doc.schema.label;
// Set Formatted Doc Link Data
await doc.loadLinks();
const links: PrintTemplateData = {};
@ -347,6 +351,7 @@ function getNameAndTypeFromTemplateFile(
* If the SchemaName is absent then it is assumed
* that the SchemaName is:
* - SalesInvoice
* - SalesQuote
* - PurchaseInvoice
*/
@ -359,12 +364,14 @@ function getNameAndTypeFromTemplateFile(
return [{ name: `${name} - ${label}`, type: schemaName }];
}
return [ModelNameEnum.SalesInvoice, ModelNameEnum.PurchaseInvoice].map(
(schemaName) => {
return [
ModelNameEnum.SalesInvoice,
ModelNameEnum.SalesQuote,
ModelNameEnum.PurchaseInvoice,
].map((schemaName) => {
const label = fyo.schemaMap[schemaName]?.label ?? schemaName;
return { name: `${name} - ${label}`, type: schemaName };
}
);
});
}
export const baseTemplate = `<main class="h-full w-full bg-white">

View File

@ -169,6 +169,12 @@ function getCompleteSidebar(): SidebarConfig {
icon: 'sales',
route: '/list/SalesInvoice',
items: [
{
label: t`Sales Quotes`,
name: 'sales-quotes',
route: '/list/SalesQuote',
schemaName: 'SalesQuote',
},
{
label: t`Sales Invoices`,
name: 'sales-invoices',

View File

@ -52,8 +52,8 @@ Accounts,Comptes,
"Accounts Payable","Comptes créditeurs",
"Accounts Receivable","Comptes débiteurs",
"Accumulated Depreciation","Amortissement cumulé",
Action,,
Active,,
Action,Action,
Active,Actif,
"Add Account","Ajouter un compte",
"Add Customers","Ajouter des clients",
"Add Group","Ajouter un groupe",
@ -689,6 +689,8 @@ Quarterly,Trimestriel,
Quarters,Trimestres,
"Quick Search",,
"Quick edit error: ${0} entry has no name.",,
"Quote","Devis",
"Quote Reference","Référence du devis",
Rate,Tarif,
"Rate (${0}) cannot be less zero.","Le Tarif (${0}) ne peut pas être inférieur à zéro.",
"Rate (${0}) has to be greater than zero",,
@ -739,6 +741,9 @@ Sales,Ventes,
"Sales Acc.",,
"Sales Expenses","Frais de vente",
"Sales Invoice","Facture de vente",
"Sales Quote","Devis de vente",
"Sales Quote Number Series",,
"Sales Quote Print Template",,
"Sales Invoice Item","Facture de vente d'article",
"Sales Invoice Number Series",,
"Sales Invoice Print Template",,
@ -792,7 +797,8 @@ September,,
Service,,
"Set Discount Amount","Définir le montant de la réduction",
"Set Period",,
"Set Print Size",,
"Set Print Size","Définir le format de page",
"Set Template Type","Définir le type de modèle",
"Set Up",Configurer,
"Set Up Your Workspace","Configurez votre espace de travail",
"Set a Template value to see the Print Template",,

1 ${0} ${1} already exists. ${0} ${1} existe déjà.
52 Accounts Payable Comptes créditeurs
53 Accounts Receivable Comptes débiteurs
54 Accumulated Depreciation Amortissement cumulé
55 Action Action
56 Active Actif
57 Add Account Ajouter un compte
58 Add Customers Ajouter des clients
59 Add Group Ajouter un groupe
689 Quarters Trimestres
690 Quick Search
691 Quick edit error: ${0} entry has no name.
692 Quote Devis
693 Quote Reference Référence du devis
694 Rate Tarif
695 Rate (${0}) cannot be less zero. Le Tarif (${0}) ne peut pas être inférieur à zéro.
696 Rate (${0}) has to be greater than zero
741 Sales Acc.
742 Sales Expenses Frais de vente
743 Sales Invoice Facture de vente
744 Sales Quote Devis de vente
745 Sales Quote Number Series
746 Sales Quote Print Template
747 Sales Invoice Item Facture de vente d'article
748 Sales Invoice Number Series
749 Sales Invoice Print Template
797 Service
798 Set Discount Amount Définir le montant de la réduction
799 Set Period
800 Set Print Size Définir le format de page
801 Set Template Type Définir le type de modèle
802 Set Up Configurer
803 Set Up Your Workspace Configurez votre espace de travail
804 Set a Template value to see the Print Template