2
0
mirror of https://github.com/frappe/books.git synced 2025-01-10 18:24:40 +00:00

Merge pull request #359 from 18alantom/custom-number-series

feat: custom number series
This commit is contained in:
Alan 2022-03-08 15:23:32 +05:30 committed by GitHub
commit b4397a9132
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 336 additions and 143 deletions

View File

@ -30,8 +30,18 @@ class SqliteDatabase extends Database {
// create temp table
await this.createTable(doctype, tempName);
// copy from old to new table
await this.knex(tempName).insert(this.knex.select().from(doctype));
try {
// copy from old to new table
await this.knex(tempName).insert(this.knex.select().from(doctype));
} catch (err) {
await this.sql('ROLLBACK');
await this.sql('PRAGMA foreign_keys=ON');
const rows = await this.knex.select().from(doctype);
await this.prestigeTheTable(doctype, rows);
return;
}
// drop old table
await this.knex.schema.dropTable(doctype);
@ -110,6 +120,7 @@ class SqliteDatabase extends Database {
// Alter table hacx for sqlite in case of schema change.
const tempName = `__${tableName}`;
await this.knex.schema.dropTableIfExists(tempName);
await this.knex.raw('PRAGMA foreign_keys=OFF');
await this.createTable(tableName, tempName);

View File

@ -1,3 +1,4 @@
const { getPaddedName } = require('@/utils');
const frappe = require('frappe');
const { getRandomString } = require('frappe/utils');
@ -28,11 +29,23 @@ module.exports = {
return;
}
// Current, per doc number series
if (doc.numberSeries) {
doc.name = await this.getSeriesNext(doc.numberSeries, doc.doctype);
return;
}
// if(doc.meta)
// Legacy, using doc settings for number series
if (doc.meta.settings) {
const numberSeries = (await doc.getSettings()).numberSeries;
if (numberSeries) {
doc.name = await this.getSeriesNext(numberSeries, doc.doctype);
if (!numberSeries) {
return;
}
doc.name = await this.getSeriesNext(numberSeries, doc.doctype);
return;
}
}
@ -78,33 +91,34 @@ module.exports = {
async getSeriesNext(prefix, doctype) {
let series;
try {
series = await frappe.getDoc('NumberSeries', prefix);
} catch (e) {
if (!e.statusCode || e.statusCode !== 404) {
throw e;
}
await this.createNumberSeries(prefix);
await this.createNumberSeries(prefix, doctype);
series = await frappe.getDoc('NumberSeries', prefix);
}
let next = await series.next(doctype);
return prefix + next;
return await series.next(doctype);
},
async createNumberSeries(prefix, setting, start = 1001) {
if (!(await frappe.db.exists('NumberSeries', prefix))) {
const series = frappe.newDoc({
doctype: 'NumberSeries',
name: prefix,
current: start,
});
await series.insert();
if (setting) {
const settingDoc = await frappe.getSingle(setting);
settingDoc.numberSeries = series.name;
await settingDoc.update();
}
async createNumberSeries(prefix, referenceType, start = 1001) {
const exists = await frappe.db.exists('NumberSeries', prefix);
if (exists) {
return;
}
const series = frappe.newDoc({
doctype: 'NumberSeries',
name: prefix,
start,
referenceType,
});
await series.insert();
},
};

View File

@ -1,5 +1,18 @@
const { t } = require('frappe');
const referenceTypeMap = {
SalesInvoice: t`Invoice`,
PurchaseInvoice: t`Bill`,
Payment: t`Payment`,
JournalEntry: t`Journal Entry`,
Quotation: t`Quotation`,
SalesOrder: t`SalesOrder`,
Fulfillment: t`Fulfillment`,
PurchaseOrder: t`PurchaseOrder`,
PurchaseReceipt: t`PurchaseReceipt`,
'-': t`None`,
};
module.exports = {
name: 'NumberSeries',
documentClass: require('./NumberSeriesDocument.js'),
@ -14,11 +27,38 @@ module.exports = {
fieldtype: 'Data',
required: 1,
},
{
fieldname: 'start',
label: t`Start`,
fieldtype: 'Int',
default: 1001,
required: 1,
minvalue: 0,
},
{
fieldname: 'padZeros',
label: t`Pad Zeros`,
fieldtype: 'Int',
default: 4,
required: 1,
},
{
fieldname: 'referenceType',
label: t`Reference Type`,
fieldtype: 'Select',
options: Object.keys(referenceTypeMap),
map: referenceTypeMap,
default: '-',
required: 1,
readOnly: 1,
},
{
fieldname: 'current',
label: t`Current`,
fieldtype: 'Int',
required: 1,
readOnly: 1,
},
],
quickEditFields: ['start', 'padZeros', 'referenceType'],
};

View File

@ -1,12 +1,14 @@
const { getPaddedName } = require('@/utils');
const frappe = require('frappe');
const BaseDocument = require('frappe/model/document');
module.exports = class NumberSeries extends BaseDocument {
validate() {
if (this.current === null || this.current === undefined) {
this.current = 0;
if (!this.current) {
this.current = this.start;
}
}
async next(doctype) {
this.validate();
@ -17,13 +19,19 @@ module.exports = class NumberSeries extends BaseDocument {
this.current++;
await this.update();
return this.current;
return this.getPaddedName(this.current);
}
async checkIfCurrentExists(doctype) {
if (!doctype) {
return true;
}
return await frappe.db.exists(doctype, this.name + this.current);
const name = this.getPaddedName(this.current);
return await frappe.db.exists(doctype, name);
}
getPaddedName(next) {
return getPaddedName(this.name, next, this.padZeros);
}
};

View File

@ -1,4 +1,15 @@
export const DEFAULT_INTERNAL_PRECISION = 11;
export const DEFAULT_DISPLAY_PRECISION = 2;
export const DEFAULT_LOCALE = 'en-IN';
export const DEFAULT_LANGUAGE = 'English';
export const DEFAULT_LANGUAGE = 'English';
export const DEFAULT_NUMBER_SERIES = {
SalesInvoice: 'SINV-',
PurchaseInvoice: 'PINV-',
Payment: 'PAY-',
JournalEntry: 'JV-',
Quotation: 'QTN-',
SalesOrder: 'SO-',
Fulfillment: 'OF-',
PurchaseOrder: 'PO-',
PurchaseReceipt: 'PREC-',
};

View File

@ -1,5 +1,6 @@
import { t } from 'frappe';
import model from 'frappe/model';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
import Quotation from '../Quotation/Quotation';
export default model.extend(Quotation, {
@ -11,5 +12,16 @@ export default model.extend(Quotation, {
fieldname: 'items',
childtype: 'FulfillmentItem',
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'Fulfillment' };
},
default: DEFAULT_NUMBER_SERIES['FulFillment'],
},
],
});

View File

@ -5,10 +5,5 @@ import QuotationSettings from '../QuotationSettings/QuotationSettings';
export default model.extend(QuotationSettings, {
name: 'FulfillmentSettings',
label: t`Fulfillment Settings`,
fields: [
{
fieldname: 'numberSeries',
default: 'OF',
},
],
fields: [],
});

View File

@ -1,6 +1,7 @@
import { t } from 'frappe';
import { DateTime } from 'luxon';
import { ledgerLink } from '../../../accounting/utils';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
export default {
label: t`Journal Entry`,
@ -72,6 +73,17 @@ export default {
default: 0,
readOnly: 1,
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'JournalEntry' };
},
default: DEFAULT_NUMBER_SERIES['JournalEntry'],
},
],
actions: [ledgerLink],
};

View File

@ -6,14 +6,5 @@ export default {
isSingle: 1,
isChild: 0,
keywordFields: [],
fields: [
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
default: 'JV',
},
],
fields: [],
};

View File

@ -1,5 +1,6 @@
import frappe, { t } from 'frappe';
import utils from '../../../accounting/utils';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
export default {
name: 'Payment',
@ -81,6 +82,17 @@ export default {
}
},
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'Payment' };
},
default: DEFAULT_NUMBER_SERIES['Payment'],
},
{
fieldname: 'paymentMethod',
label: t`Payment Method`,
@ -162,6 +174,7 @@ export default {
],
quickEditFields: [
'numberSeries',
'party',
'date',
'paymentMethod',

View File

@ -6,14 +6,5 @@ export default {
isSingle: 1,
isChild: 0,
keywordFields: [],
fields: [
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
default: 'PAY',
},
],
fields: [],
};

View File

@ -1,4 +1,5 @@
import { t } from 'frappe';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
import InvoiceTemplate from '../SalesInvoice/InvoiceTemplate.vue';
import { getActions } from '../Transaction/Transaction';
import PurchaseInvoice from './PurchaseInvoiceDocument';
@ -134,6 +135,17 @@ export default {
default: 0,
readOnly: 1,
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'PurchaseInvoice' };
},
default: DEFAULT_NUMBER_SERIES['PurchaseInvoice'],
},
],
actions: getActions('PurchaseInvoice'),

View File

@ -6,14 +6,5 @@ export default {
isSingle: 1,
isChild: 0,
keywordFields: [],
fields: [
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
default: 'PINV',
},
],
fields: [],
};

View File

@ -1,5 +1,6 @@
import { t } from 'frappe';
import model from 'frappe/model';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
import PurchaseInvoice from '../PurchaseInvoice/PurchaseInvoice';
export default model.extend(
@ -13,6 +14,17 @@ export default model.extend(
fieldname: 'items',
childtype: 'PurchaseOrderItem',
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'PurchaseOrder' };
},
default: DEFAULT_NUMBER_SERIES['PurchaseOrder'],
},
],
},
{

View File

@ -5,10 +5,5 @@ import PurchaseInvoiceSettings from '../PurchaseInvoiceSettings/PurchaseInvoiceS
export default model.extend(PurchaseInvoiceSettings, {
name: 'PurchaseOrderSettings',
label: t`Purchase Order Settings`,
fields: [
{
fieldname: 'numberSeries',
default: 'PO',
},
],
fields: [],
});

View File

@ -1,5 +1,6 @@
import { t } from 'frappe';
import model from 'frappe/model';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
import PurchaseOrder from '../PurchaseOrder/PurchaseOrder';
export default model.extend(PurchaseOrder, {
@ -11,5 +12,16 @@ export default model.extend(PurchaseOrder, {
fieldname: 'items',
childtype: 'PurchaseReceiptItem',
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'PurchaseReceipt' };
},
default: DEFAULT_NUMBER_SERIES['PurchaseReceipt'],
},
],
});

View File

@ -5,10 +5,5 @@ import PurchaseOrderSettings from '../PurchaseOrderSettings/PurchaseOrderSetting
export default model.extend(PurchaseOrderSettings, {
name: 'PurchaseReceiptSettings',
label: t`Purchase Receipt Settings`,
fields: [
{
fieldname: 'numberSeries',
default: 'PREC',
},
],
fields: [],
});

View File

@ -1,5 +1,6 @@
import { t } from 'frappe';
import model from 'frappe/model';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
import SalesInvoice from '../SalesInvoice/SalesInvoice';
const Quotation = model.extend(
@ -13,6 +14,17 @@ const Quotation = model.extend(
fieldname: 'items',
childtype: 'QuotationItem',
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'Quotation' };
},
default: DEFAULT_NUMBER_SERIES['Quotation'],
},
],
links: [],
},

View File

@ -5,10 +5,5 @@ import SalesInvoiceSettings from '../SalesInvoiceSettings/SalesInvoiceSettings';
export default model.extend(SalesInvoiceSettings, {
name: 'QuotationSettings',
label: t`Quotation Settings`,
fields: [
{
fieldname: 'numberSeries',
default: 'QTN',
},
],
fields: [],
});

View File

@ -1,4 +1,5 @@
import { t } from 'frappe';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
import { getActions } from '../Transaction/Transaction';
import InvoiceTemplate from './InvoiceTemplate.vue';
import SalesInvoice from './SalesInvoiceDocument';
@ -134,6 +135,17 @@ export default {
default: 0,
readOnly: 1,
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'SalesInvoice' };
},
default: DEFAULT_NUMBER_SERIES['SalesInvoice'],
},
],
actions: getActions('SalesInvoice'),

View File

@ -7,14 +7,6 @@ export default {
isChild: 0,
keywordFields: [],
fields: [
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
default: 'SINV',
},
{
fieldname: 'template',
label: t`Template`,

View File

@ -1,5 +1,6 @@
import { t } from 'frappe';
import model from 'frappe/model';
import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts';
import Quotation from '../Quotation/Quotation';
export default model.extend(Quotation, {
@ -11,5 +12,16 @@ export default model.extend(Quotation, {
fieldname: 'items',
childtype: 'SalesOrderItem',
},
{
fieldname: 'numberSeries',
label: t`Number Series`,
fieldtype: 'Link',
target: 'NumberSeries',
required: 1,
getFilters: () => {
return { referenceType: 'SalesOrder' };
},
default: DEFAULT_NUMBER_SERIES['SalesOrder'],
},
],
});

View File

@ -5,10 +5,5 @@ import QuotationSettings from '../QuotationSettings/QuotationSettings';
export default model.extend(QuotationSettings, {
name: 'SalesOrderSettings',
label: t`Sales Order Settings`,
fields: [
{
fieldname: 'numberSeries',
default: 'SO',
},
],
fields: [],
});

View File

@ -0,0 +1,20 @@
import { invertMap } from '@/utils';
import frappe from 'frappe';
import { DEFAULT_NUMBER_SERIES } from 'frappe/utils/consts';
async function setReferencesOnNumberSeries() {
const map = invertMap(DEFAULT_NUMBER_SERIES);
const rows = await frappe.db.knex('NumberSeries');
for (const row of rows) {
if (row.referenceType === map[row.name]) {
return;
}
row.referenceType = map[row.name];
}
await frappe.db.prestigeTheTable('NumberSeries', rows);
}
export default async function execute() {
await setReferencesOnNumberSeries();
}

View File

@ -8,5 +8,10 @@
"version": "0.0.4",
"fileName": "convertCurrencyToStrings",
"beforeMigrate": true
},
{
"version": "0.3.2",
"fileName": "moveNumberSeriesFromSettings",
"beforeMigrate": false
}
]

View File

@ -20,15 +20,15 @@ export default async function postStart() {
frappe.metaCache = {};
// init naming series if missing
await naming.createNumberSeries('SINV-', 'SalesInvoiceSettings');
await naming.createNumberSeries('PINV-', 'PurchaseInvoiceSettings');
await naming.createNumberSeries('PAY-', 'PaymentSettings');
await naming.createNumberSeries('JV-', 'JournalEntrySettings');
await naming.createNumberSeries('QTN-', 'QuotationSettings');
await naming.createNumberSeries('SO-', 'SalesOrderSettings');
await naming.createNumberSeries('OF-', 'FulfillmentSettings');
await naming.createNumberSeries('PO-', 'PurchaseOrderSettings');
await naming.createNumberSeries('PREC-', 'PurchaseReceiptSettings');
await naming.createNumberSeries('SINV-', 'SalesInvoice');
await naming.createNumberSeries('PINV-', 'PurchaseInvoice');
await naming.createNumberSeries('PAY-', 'Payment');
await naming.createNumberSeries('JV-', 'JournalEntry');
// await naming.createNumberSeries('QTN-', 'QuotationSettings');
// await naming.createNumberSeries('SO-', 'SalesOrderSettings');
// await naming.createNumberSeries('OF-', 'FulfillmentSettings');
// await naming.createNumberSeries('PO-', 'PurchaseOrderSettings');
// await naming.createNumberSeries('PREC-', 'PurchaseReceiptSettings');
// fetch singles
// so that they are available synchronously

View File

@ -1,10 +1,9 @@
<script>
import frappe from 'frappe';
import AutoComplete from './AutoComplete';
import Badge from '@/components/Badge';
import { openQuickEdit } from '@/utils';
import frappe, { t } from 'frappe';
import { markRaw } from 'vue';
import { t } from 'frappe';
import AutoComplete from './AutoComplete';
export default {
name: 'Link',
@ -84,12 +83,20 @@ export default {
};
},
async getFilters(keyword) {
if (this.doc) {
return this.df.getFilters
? (await this.df.getFilters(keyword, this.doc)) || {}
: {};
const { getFilters } = this.df;
if (!getFilters) {
return {};
}
if (this.doc) {
return (await getFilters(keyword, this.doc)) ?? {};
}
try {
return (await getFilters()) ?? {};
} catch {
return {};
}
return {};
},
getTarget() {
return this.df.target;

View File

@ -103,6 +103,14 @@
@change="(value) => doc.set('date', value)"
:read-only="doc.submitted"
/>
<FormControl
class="mt-2 text-base"
input-class="bg-gray-100 px-3 py-2 text-base text-right"
:df="meta.getField('numberSeries')"
:value="doc.numberSeries"
@change="(value) => doc.set('numberSeries', value)"
:read-only="doc.submitted"
/>
</div>
</div>
</div>
@ -190,20 +198,20 @@
</div>
</template>
<script>
import frappe from 'frappe';
import StatusBadge from '@/components/StatusBadge';
import PageHeader from '@/components/PageHeader';
import BackLink from '@/components/BackLink';
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import DropdownWithActions from '@/components/DropdownWithActions';
import BackLink from '@/components/BackLink';
import PageHeader from '@/components/PageHeader';
import StatusBadge from '@/components/StatusBadge';
import {
openSettings,
getActionsForDocument,
getInvoiceStatus,
showMessageDialog,
routeTo,
getActionsForDocument,
getInvoiceStatus,
openSettings,
routeTo,
showMessageDialog
} from '@/utils';
import frappe from 'frappe';
import { handleErrorWithDialog } from '../errorHandling';
export default {

View File

@ -35,7 +35,7 @@
<h1 class="text-2xl font-semibold">
{{ doc._notInserted ? t`New Journal Entry` : doc.name }}
</h1>
<div class="flex justify-between mt-2">
<div class="flex justify-between mt-2 gap-2">
<div class="w-1/3">
<FormControl
:df="meta.getField('entryType')"
@ -78,6 +78,16 @@
:class="doc.submitted && 'pointer-events-none'"
/>
</div>
<div class="w-1/3">
<FormControl
:df="meta.getField('numberSeries')"
:value="doc.numberSeries"
@change="(value) => doc.set('numberSeries', value)"
input-class="bg-gray-100 p-2 text-base"
:read-only="doc.submitted"
:class="doc.submitted && 'pointer-events-none'"
/>
</div>
</div>
</div>
<div class="mt-2 px-6 text-base">
@ -118,14 +128,14 @@
</div>
</template>
<script>
import frappe from 'frappe';
import PageHeader from '@/components/PageHeader';
import Button from '@/components/Button';
import DropdownWithActions from '@/components/DropdownWithActions';
import FormControl from '@/components/Controls/FormControl';
import BackLink from '@/components/BackLink';
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import DropdownWithActions from '@/components/DropdownWithActions';
import PageHeader from '@/components/PageHeader';
import StatusBadge from '@/components/StatusBadge';
import { showMessageDialog, getActionsForDocument, routeTo } from '@/utils';
import { getActionsForDocument, routeTo, showMessageDialog } from '@/utils';
import frappe from 'frappe';
import { handleErrorWithDialog } from '../errorHandling';
export default {

View File

@ -536,3 +536,23 @@ export function getCOAList() {
}
return frappe.temp.coaList;
}
export function invertMap(map) {
const keys = Object.keys(map);
const inverted = {};
for (const key of keys) {
const val = map[key];
inverted[val] = key;
}
return inverted;
}
export function getPaddedName(prefix, next, padZeros) {
const padding = padZeros ?? 4;
const l = next.toString().length;
const z = '0'.repeat(Math.max(0, padding - l));
return prefix + z + next;
}

View File

@ -12,20 +12,11 @@
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"types": ["webpack-env"],
"paths": {
"@/*": [
"src/*"
]
"@/*": ["src/*"]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": [
"src/**/*.ts",
@ -38,11 +29,10 @@
"frappe/**/*.ts",
"models/**/*.ts",
"patches/**/*.ts",
"patches/**/*.js",
"reports/**/*.ts",
"accounting/**/*.ts",
"accounting/**/*.ts",
"accounting/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}