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

incr: add Purchase Recipt & Shipment scaffolding

This commit is contained in:
18alantom 2022-11-14 14:00:11 +05:30
parent a9fd590512
commit cf37077b6d
24 changed files with 666 additions and 19 deletions

View File

@ -2,19 +2,33 @@ import { getDefaultMetaFieldValueMap } from '../../backend/helpers';
import { DatabaseManager } from '../database/manager';
async function execute(dm: DatabaseManager) {
const schemaName = 'NumberSeries';
const name = 'SMOV-';
const exists = await dm.db?.exists(schemaName, name);
const names: Record<string, string> = {
StockMovement: 'SMOV-',
Shipment: 'SHP-',
};
for (const referenceType in names) {
const name = names[referenceType];
await createNumberSeries(name, referenceType, dm);
}
}
async function createNumberSeries(
name: string,
referenceType: string,
dm: DatabaseManager
) {
const exists = await dm.db?.exists('NumberSeries', name);
if (exists) {
return;
}
await dm.db?.insert(schemaName, {
await dm.db?.insert('NumberSeries', {
name,
start: 1001,
padZeros: 4,
current: 0,
referenceType: 'StockMovement',
referenceType,
...getDefaultMetaFieldValueMap(),
});
}

View File

@ -102,7 +102,7 @@ export class Converter {
}
#toDocValueMap(schemaName: string, rawValueMap: RawValueMap): DocValueMap {
const fieldValueMap = this.db.fieldValueMap[schemaName];
const fieldValueMap = this.db.fieldMap[schemaName];
const docValueMap: DocValueMap = {};
for (const fieldname in rawValueMap) {
@ -130,7 +130,7 @@ export class Converter {
}
#toRawValueMap(schemaName: string, docValueMap: DocValueMap): RawValueMap {
const fieldValueMap = this.db.fieldValueMap[schemaName];
const fieldValueMap = this.db.fieldMap[schemaName];
const rawValueMap: RawValueMap = {};
for (const fieldname in docValueMap) {

View File

@ -10,7 +10,7 @@ import {
DatabaseBase,
DatabaseDemuxBase,
GetAllOptions,
QueryFilter,
QueryFilter
} from 'utils/db/types';
import { schemaTranslateables } from 'utils/translationHelpers';
import { LanguageMap } from 'utils/types';
@ -19,7 +19,7 @@ import {
DatabaseDemuxConstructor,
DocValue,
DocValueMap,
RawValueMap,
RawValueMap
} from './types';
// Return types of Bespoke Queries
@ -33,6 +33,7 @@ type TotalCreditAndDebit = {
totalCredit: number;
totalDebit: number;
};
type FieldMap = Record<string, Record<string, Field>>;
export class DatabaseHandler extends DatabaseBase {
#fyo: Fyo;
@ -40,8 +41,8 @@ export class DatabaseHandler extends DatabaseBase {
#demux: DatabaseDemuxBase;
dbPath?: string;
#schemaMap: SchemaMap = {};
#fieldMap: FieldMap = {};
observer: Observable<never> = new Observable();
fieldValueMap: Record<string, Record<string, Field>> = {};
constructor(fyo: Fyo, Demux?: DatabaseDemuxConstructor) {
super();
@ -59,6 +60,10 @@ export class DatabaseHandler extends DatabaseBase {
return this.#schemaMap;
}
get fieldMap(): Readonly<FieldMap> {
return this.#fieldMap;
}
get isConnected() {
return !!this.dbPath;
}
@ -79,11 +84,7 @@ export class DatabaseHandler extends DatabaseBase {
async init() {
this.#schemaMap = (await this.#demux.getSchemaMap()) as SchemaMap;
for (const schemaName in this.schemaMap) {
const fields = this.schemaMap[schemaName]!.fields!;
this.fieldValueMap[schemaName] = getMapFromList(fields, 'fieldname');
}
this.#setFieldMap();
this.observer = new Observable();
}
@ -92,6 +93,7 @@ export class DatabaseHandler extends DatabaseBase {
translateSchema(this.#schemaMap, languageMap, schemaTranslateables);
} else {
this.#schemaMap = (await this.#demux.getSchemaMap()) as SchemaMap;
this.#setFieldMap();
}
}
@ -99,7 +101,7 @@ export class DatabaseHandler extends DatabaseBase {
await this.close();
this.dbPath = undefined;
this.#schemaMap = {};
this.fieldValueMap = {};
this.#fieldMap = {};
}
async insert(
@ -166,7 +168,7 @@ export class DatabaseHandler extends DatabaseBase {
const docSingleValue: SingleValue<DocValue> = [];
for (const sv of rawSingleValue) {
const field = this.fieldValueMap[sv.parent][sv.fieldname];
const field = this.fieldMap[sv.parent][sv.fieldname];
const value = Converter.toDocValue(sv.value, field, this.#fyo);
docSingleValue.push({
@ -334,4 +336,15 @@ export class DatabaseHandler extends DatabaseBase {
options
)) as RawValueMap[];
}
#setFieldMap() {
this.#fieldMap = Object.values(this.schemaMap).reduce((acc, sch) => {
if (!sch?.name) {
return acc;
}
acc[sch?.name] = getMapFromList(sch?.fields, 'fieldname');
return acc;
}, {} as FieldMap);
}
}

View File

@ -84,6 +84,10 @@ export class Fyo {
return this.db.schemaMap;
}
get fieldMap() {
return this.db.fieldMap;
}
format(value: DocValue, field: string | Field, doc?: Doc) {
return format(value, field, doc ?? null, this);
}
@ -163,8 +167,7 @@ export class Fyo {
}
getField(schemaName: string, fieldname: string) {
const schema = this.schemaMap[schemaName];
return schema?.fields.find((f) => f.fieldname === fieldname);
return this.fieldMap[schemaName][fieldname];
}
async getValue(

View File

@ -18,6 +18,10 @@ import { SetupWizard } from './baseModels/SetupWizard/SetupWizard';
import { Tax } from './baseModels/Tax/Tax';
import { TaxSummary } from './baseModels/TaxSummary/TaxSummary';
import { Location } from './inventory/Location';
import { PurchaseReceipt } from './inventory/PurchaseReceipt';
import { PurchaseReceiptItem } from './inventory/PurchaseReceiptItem';
import { Shipment } from './inventory/Shipment';
import { ShipmentItem } from './inventory/ShipmentItem';
import { StockLedgerEntry } from './inventory/StockLedgerEntry';
import { StockMovement } from './inventory/StockMovement';
import { StockMovementItem } from './inventory/StockMovementItem';
@ -46,6 +50,10 @@ export const models = {
StockMovementItem,
StockLedgerEntry,
Location,
Shipment,
ShipmentItem,
PurchaseReceipt,
PurchaseReceiptItem,
} as ModelMap;
export async function getRegionalModels(

View File

@ -0,0 +1,21 @@
import { ListViewSettings } from 'fyo/model/types';
import { getTransactionStatusColumn } from 'models/helpers';
import { PurchaseReceiptItem } from './PurchaseReceiptItem';
import { StockTransfer } from './StockTransfer';
export class PurchaseReceipt extends StockTransfer {
items?: PurchaseReceiptItem[];
static getListViewSettings(): ListViewSettings {
return {
formRoute: (name) => `/edit/PurchaseReceipt/${name}`,
columns: [
'name',
getTransactionStatusColumn(),
'party',
'date',
'grandTotal',
],
};
}
}

View File

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

View File

@ -0,0 +1,21 @@
import { ListViewSettings } from 'fyo/model/types';
import { getTransactionStatusColumn } from 'models/helpers';
import { ShipmentItem } from './ShipmentItem';
import { StockTransfer } from './StockTransfer';
export class Shipment extends StockTransfer {
items?: ShipmentItem[];
static getListViewSettings(): ListViewSettings {
return {
formRoute: (name) => `/edit/Shipment/${name}`,
columns: [
'name',
getTransactionStatusColumn(),
'party',
'date',
'grandTotal',
],
};
}
}

View File

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

View File

@ -0,0 +1,10 @@
import { Attachment } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
export abstract class StockTransfer extends Doc {
name?: string;
date?: string;
party?: string;
terms?: string;
attachment?: Attachment;
}

View File

@ -0,0 +1,13 @@
import { Doc } from 'fyo/model/doc';
import { Money } from 'pesa';
export class StockTransferItem extends Doc {
item?: string;
location?: string;
quantity?: number;
rate?: Money;
amount?: Money;
unit?: string;
description?: string;
hsnCode?: number;
}

View File

@ -33,6 +33,10 @@ export enum ModelNameEnum {
StockMovement = 'StockMovement',
StockMovementItem = 'StockMovementItem',
StockLedgerEntry = 'StockLedgerEntry',
Shipment = 'Shipment',
ShipmentItem = 'ShipmentItem',
PurchaseReceipt = 'PurchaseReceipt',
PurchaseReceiptItem = 'PurchaseReceiptItem',
Location = 'Location',
}

View File

@ -50,6 +50,14 @@
{
"value": "StockMovement",
"label": "Stock Movement"
},
{
"value": "Shipment",
"label": "Shipment"
},
{
"value": "PurchaseReceipt",
"label": "Purchase Receipt"
}
],
"default": "-",

View File

@ -0,0 +1,27 @@
{
"name": "PurchaseReceipt",
"label": "Purchase Receipt",
"extends": "StockTransfer",
"naming": "numberSeries",
"showTitle": true,
"fields": [
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"target": "PurchaseReceiptItem",
"required": true,
"edit": true
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "PREC-"
}
],
"keywordFields": ["name", "party"]
}

View File

@ -0,0 +1,14 @@
{
"name": "PurchaseReceiptItem",
"label": "Purchase Receipt Item",
"extends": "StockTransferItem",
"fields": [
{
"fieldname": "location",
"label": "Dest.",
"fieldtype": "Link",
"target": "Location",
"required": true
}
]
}

View File

@ -0,0 +1,27 @@
{
"name": "Shipment",
"label": "Shipment",
"extends": "StockTransfer",
"naming": "numberSeries",
"showTitle": true,
"fields": [
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"target": "ShipmentItem",
"required": true,
"edit": true
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "SHPM-"
}
],
"keywordFields": ["name", "party"]
}

View File

@ -0,0 +1,14 @@
{
"name": "ShipmentItem",
"label": "Shipment Item",
"extends": "StockTransferItem",
"fields": [
{
"fieldname": "location",
"label": "Source",
"fieldtype": "Link",
"target": "Location",
"required": true
}
]
}

View File

@ -0,0 +1,50 @@
{
"name": "StockTransfer",
"label": "StockTransfer",
"isAbstract": true,
"isSingle": false,
"isChild": false,
"isSubmittable": true,
"fields": [
{
"label": "Transfer No",
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
},
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date",
"required": true
},
{
"fieldname": "party",
"label": "Party",
"fieldtype": "Link",
"target": "Party",
"create": true,
"required": true
},
{
"fieldname": "terms",
"label": "Notes",
"placeholder": "Add transfer terms",
"fieldtype": "Text"
},
{
"fieldname": "attachment",
"placeholder": "Add attachment",
"label": "Attachment",
"fieldtype": "Attachment"
},
{
"fieldname": "grandTotal",
"label": "Grand Total",
"fieldtype": "Currency",
"readOnly": true
}
],
"keywordFields": ["name", "party"]
}

View File

@ -0,0 +1,71 @@
{
"name": "StockTransferItem",
"label": "Stock Transfer Item",
"isAbstract": true,
"isChild": true,
"fields": [
{
"fieldname": "item",
"label": "Item",
"fieldtype": "Link",
"target": "Item",
"required": true
},
{
"fieldname": "location",
"fieldtype": "Link",
"target": "Location",
"required": true
},
{
"fieldname": "quantity",
"label": "Quantity",
"fieldtype": "Float",
"required": true,
"default": 1
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Currency",
"required": true
},
{
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "unit",
"label": "Unit Type",
"fieldtype": "Link",
"target": "UOM",
"default": "Unit",
"placeholder": "Unit Type"
},
{
"fieldname": "description",
"label": "Description",
"placeholder": "Item Description",
"fieldtype": "Text"
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
}
],
"tableFields": ["item", "location", "quantity", "rate", "amount"],
"quickEditFields": [
"item",
"unit",
"description",
"hsnCode",
"location",
"quantity",
"rate",
"amount"
]
}

View File

@ -8,9 +8,15 @@ import Currency from './app/Currency.json';
import Defaults from './app/Defaults.json';
import GetStarted from './app/GetStarted.json';
import Location from './app/inventory/Location.json';
import PurchaseReceipt from './app/inventory/PurchaseReceipt.json';
import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.json';
import Shipment from './app/inventory/Shipment.json';
import ShipmentItem from './app/inventory/ShipmentItem.json';
import StockLedgerEntry from './app/inventory/StockLedgerEntry.json';
import StockMovement from './app/inventory/StockMovement.json';
import StockMovementItem from './app/inventory/StockMovementItem.json';
import StockTransfer from './app/inventory/StockTransfer.json';
import StockTransferItem from './app/inventory/StockTransferItem.json';
import Invoice from './app/Invoice.json';
import InvoiceItem from './app/InvoiceItem.json';
import Item from './app/Item.json';
@ -97,4 +103,11 @@ export const appSchemas: Schema[] | SchemaStub[] = [
StockLedgerEntry as Schema,
StockMovement as Schema,
StockMovementItem as Schema,
StockTransfer as Schema,
StockTransferItem as Schema,
Shipment as Schema,
ShipmentItem as Schema,
PurchaseReceipt as Schema,
PurchaseReceiptItem as Schema,
];

View File

@ -38,6 +38,9 @@ const components = {
export default {
name: 'FormControl',
render() {
if (!this.$attrs.df) {
console.log(this);
}
const fieldtype = this.$attrs.df.fieldtype;
const component = components[fieldtype] ?? Data;

267
src/pages/GeneralForm.vue Normal file
View File

@ -0,0 +1,267 @@
<template>
<FormContainer>
<!-- Page Header (Title, Buttons, etc) -->
<template #header v-if="doc">
<StatusBadge :status="status" />
<DropdownWithActions :actions="actions()" />
<Button
v-if="doc?.notInserted || doc?.dirty"
type="primary"
@click="sync"
>
{{ t`Save` }}
</Button>
<Button
v-if="!doc?.dirty && !doc?.notInserted && !doc?.submitted"
type="primary"
@click="submit"
>{{ t`Submit` }}</Button
>
</template>
<!-- Form Header -->
<template #body v-if="doc">
<FormHeader
:form-title="doc.notInserted ? t`New Entry` : doc.name"
:form-sub-title="doc.schema?.label ?? ''"
/>
<hr />
<div>
<!-- Invoice Form Data Entry -->
<div class="m-4 grid grid-cols-3 gap-4">
<FormControl
input-class="font-semibold"
:border="true"
:df="getField('party')"
:value="doc.party"
@change="(value) => doc.set('party', value, true)"
@new-doc="(party) => doc.set('party', party.name, true)"
:read-only="doc?.submitted"
/>
<FormControl
input-class="text-right"
:border="true"
:df="getField('date')"
:value="doc.date"
@change="(value) => doc.set('date', value)"
:read-only="doc?.submitted"
/>
<FormControl
input-class="text-right"
:border="true"
:df="getField('numberSeries')"
:value="doc.numberSeries"
@change="(value) => doc.set('numberSeries', value)"
:read-only="!doc.notInserted || doc?.submitted"
/>
<FormControl
v-if="doc.attachment || !(doc.isSubmitted || doc.isCancelled)"
:border="true"
:df="getField('attachment')"
:value="doc.attachment"
@change="(value) => doc.set('attachment', value)"
:read-only="doc?.submitted"
/>
</div>
<hr />
<!-- Invoice Items Table -->
<Table
class="text-base"
:df="getField('items')"
:value="doc.items"
:showHeader="true"
:max-rows-before-overflow="4"
@change="(value) => doc.set('items', value)"
@editrow="toggleQuickEditDoc"
:read-only="doc?.submitted"
/>
</div>
<!-- Invoice Form Footer -->
<div v-if="doc.items?.length ?? 0" class="mt-auto">
<hr />
<div class="flex justify-between text-base m-4 gap-12">
<div class="w-1/2 flex flex-col justify-between">
<!-- Form Terms-->
<FormControl
:border="true"
v-if="!doc?.submitted || doc.terms"
:df="getField('terms')"
:value="doc.terms"
class="mt-auto"
@change="(value) => doc.set('terms', value)"
:read-only="doc?.submitted"
/>
</div>
</div>
</div>
</template>
<template #quickedit v-if="quickEditDoc">
<QuickEditForm
class="w-quick-edit"
:name="quickEditDoc.name"
:show-name="false"
:show-save="false"
:source-doc="quickEditDoc"
:source-fields="quickEditFields"
:schema-name="quickEditDoc.schemaName"
:white="true"
:route-back="false"
:load-on-close="false"
@close="toggleQuickEditDoc(null)"
/>
</template>
</FormContainer>
</template>
<script>
import { computed } from '@vue/reactivity';
import { getDocStatus } from 'models/helpers';
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import Table from 'src/components/Controls/Table.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
import {
docsPath,
getActionsForDocument,
routeTo,
showMessageDialog,
} from 'src/utils/ui';
import { nextTick } from 'vue';
import { handleErrorWithDialog } from '../errorHandling';
import QuickEditForm from './QuickEditForm.vue';
export default {
name: 'InvoiceForm',
props: { schemaName: String, name: String },
components: {
StatusBadge,
Button,
FormControl,
DropdownWithActions,
Table,
FormContainer,
QuickEditForm,
FormHeader,
},
provide() {
return {
schemaName: this.schemaName,
name: this.name,
doc: computed(() => this.doc),
};
},
data() {
return {
chstatus: false,
doc: null,
quickEditDoc: null,
quickEditFields: [],
printSettings: null,
};
},
updated() {
this.chstatus = !this.chstatus;
},
computed: {
status() {
this.chstatus;
return getDocStatus(this.doc);
},
},
activated() {
docsPath.value = docsPathMap[this.schemaName];
},
deactivated() {
docsPath.value = '';
},
async mounted() {
try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
} catch (error) {
if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`);
return;
}
await this.handleError(error);
}
let query = this.$route.query;
if (query.values && query.schemaName === this.schemaName) {
this.doc.set(this.$router.currentRoute.value.query.values);
}
if (fyo.store.isDevelopment) {
window.frm = this;
}
},
methods: {
routeTo,
async toggleQuickEditDoc(doc, fields = []) {
if (this.quickEditDoc && doc) {
this.quickEditDoc = null;
this.quickEditFields = [];
await nextTick();
}
this.quickEditDoc = doc;
this.quickEditFields = fields;
},
actions() {
return getActionsForDocument(this.doc);
},
getField(fieldname) {
return fyo.getField(this.schemaName, fieldname);
},
async sync() {
try {
await this.doc.sync();
} catch (err) {
await this.handleError(err);
}
},
async submit() {
const message = t`Submit ${this.doc.name}`;
const ref = this;
await showMessageDialog({
message,
buttons: [
{
label: this.t`Yes`,
async action() {
try {
await ref.doc.submit();
} catch (err) {
await ref.handleError(err);
}
},
},
{
label: this.t`No`,
action() {},
},
],
});
},
async handleError(e) {
await handleErrorWithDialog(e, this.doc);
},
formattedValue(fieldname, doc) {
if (!doc) {
doc = this.doc;
}
const df = this.getField(fieldname);
return fyo.format(doc[fieldname], df, doc);
},
},
};
</script>

View File

@ -2,6 +2,7 @@ import { ModelNameEnum } from 'models/types';
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
import DataImport from 'src/pages/DataImport.vue';
import GeneralForm from 'src/pages/GeneralForm.vue';
import GetStarted from 'src/pages/GetStarted.vue';
import InvoiceForm from 'src/pages/InvoiceForm.vue';
import JournalEntryForm from 'src/pages/JournalEntryForm.vue';
@ -12,6 +13,31 @@ import Report from 'src/pages/Report.vue';
import Settings from 'src/pages/Settings/Settings.vue';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
function getGeneralFormItems(): RouteRecordRaw[] {
return [ModelNameEnum.Shipment, ModelNameEnum.PurchaseReceipt].map(
(schemaName) => {
return {
path: `/edit/${schemaName}/:name`,
name: `${schemaName}Form`,
components: {
default: GeneralForm,
edit: QuickEditForm,
},
props: {
default: (route) => {
route.params.schemaName = schemaName;
return {
schemaName,
name: route.params.name,
};
},
edit: (route) => route.query,
},
};
}
);
}
const routes: RouteRecordRaw[] = [
{
path: '/',
@ -21,6 +47,7 @@ const routes: RouteRecordRaw[] = [
path: '/get-started',
component: GetStarted,
},
...getGeneralFormItems(),
{
path: '/edit/JournalEntry/:name',
name: 'JournalEntryForm',
@ -122,6 +149,7 @@ const routes: RouteRecordRaw[] = [
},
},
];
console.log(routes);
export function getEntryRoute(schemaName: string, name: string) {
if (

View File

@ -82,6 +82,18 @@ async function getInventorySidebar(): Promise<SidebarRoot[]> {
route: '/list/StockMovement',
schemaName: 'StockMovement',
},
{
label: t`Shipment`,
name: 'shipment',
route: '/list/Shipment',
schemaName: 'Shipment',
},
{
label: t`Purchase Receipt`,
name: 'purchase-receipt',
route: '/list/PurchaseReceipt',
schemaName: 'PurchaseReceipt',
},
],
},
];