mirror of
https://github.com/frappe/books.git
synced 2024-11-08 06:44:06 +00:00
Merge branch 'master' of https://github.com/slang123/frappe-books
This commit is contained in:
commit
4a57f94bb5
@ -59,5 +59,6 @@ module.exports = {
|
||||
'vite.config.ts',
|
||||
'postcss.config.js',
|
||||
'src/components/**/*.vue', // Incrementally fix these
|
||||
'electron-builder.ts',
|
||||
],
|
||||
};
|
||||
|
99
.github/ISSUE_TEMPLATE/1-bug_template.yml
vendored
Normal file
99
.github/ISSUE_TEMPLATE/1-bug_template.yml
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
name: '🐛 Bug Report'
|
||||
description: Create a new ticket for a bug.
|
||||
title: '🐛 [Bug] - <title>'
|
||||
labels: ['bug']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: expected_behavior
|
||||
attributes:
|
||||
label: 'Expected Behavior'
|
||||
description: What was the expected behavior?
|
||||
placeholder: "..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: current_behavior
|
||||
attributes:
|
||||
label: 'Current Behavior'
|
||||
description: What is the current behavior?
|
||||
placeholder: "..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: 'Steps to Reproduce'
|
||||
description: Please try to describe the issue as best as possible
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 'FrappeBooks Version'
|
||||
# description: Please enter your GitHub URL to provide a reproduction of the issue
|
||||
placeholder: ex. 0.20.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: path_feature_name
|
||||
attributes:
|
||||
label: 'Path or Feature name'
|
||||
description: Please enter the path (i.e. /import-wizard) or Feature name where the bug was seen
|
||||
placeholder: ex. Import-Wizard
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: country_code
|
||||
attributes:
|
||||
label: 'Country'
|
||||
description: Please enter the two digit country code for your country (i.e. BR, CH, IN, US)
|
||||
placeholder: ex. IN
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: language
|
||||
attributes:
|
||||
label: 'Language'
|
||||
description: Please enter the two digit language code or full lanaguage used in the application
|
||||
placeholder: ex. EN or english
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: 'OS'
|
||||
description: What is the impacted environment ?
|
||||
multiple: true
|
||||
options:
|
||||
- Windows 8
|
||||
- Windows 8.1
|
||||
- Windows 10
|
||||
- Windows 11
|
||||
- Linux x86_64
|
||||
- Linux Arm64 (i.e. Raspberry Pi)
|
||||
- Macos (Intel)
|
||||
- Macos (Apple Silicon)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: additional_os_info
|
||||
attributes:
|
||||
label: 'Additional OS Info'
|
||||
description: Please enter any additional information regarding your OS that may aid in troubleshooting (i.e. Macos 10.14, Ubuntu 20.04, etc)
|
||||
placeholder: ex. Macos 10.14 / Ubuntu 20.04
|
||||
validations:
|
||||
required: false
|
50
.github/ISSUE_TEMPLATE/2-feature_template.yml
vendored
Normal file
50
.github/ISSUE_TEMPLATE/2-feature_template.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
name: '💡 Feature Request'
|
||||
description: Create a new ticket for a new feature request
|
||||
title: '💡 [Feature Request] - <title>'
|
||||
labels: ['enhancement']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: 'Summary'
|
||||
description: Provide a brief explanation of the feature
|
||||
placeholder: Describe in a few lines your feature request
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: benefits
|
||||
attributes:
|
||||
label: 'What problem are you trying to solve?'
|
||||
description: Tell us what is the thing you are doing and why this feature would help you in that
|
||||
placeholder: Describe the problem or issue that the feature would solve
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: basic_example
|
||||
attributes:
|
||||
label: 'Basic Example'
|
||||
description: Indicate here some basic examples of your feature.
|
||||
placeholder: A few specific words about your feature request.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: drawbacks
|
||||
attributes:
|
||||
label: 'Drawbacks'
|
||||
description: What are the drawbacks/impacts of your feature request ?
|
||||
placeholder: Identify the drawbacks and impacts while being neutral on your feature request
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reference_issues
|
||||
attributes:
|
||||
label: 'Reference Issues'
|
||||
description: Common issues
|
||||
placeholder: '#Issues IDs'
|
||||
validations:
|
||||
required: false
|
14
.github/ISSUE_TEMPLATE/3-general_question_template.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/3-general_question_template.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: 'General Question'
|
||||
description: Create a new ticket for a general question
|
||||
title: '🐛 [General Question] - <title>'
|
||||
labels: ['question']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: 'Summary'
|
||||
description: General Question(s) (for Bugs and Feature Requests, please use the appropriate template)
|
||||
placeholder: '...'
|
||||
validations:
|
||||
required: true
|
0
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
0
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '18.19.0'
|
||||
|
||||
- name: Set yarn version
|
||||
run: yarn set version 1.22.18
|
||||
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '18.19.0'
|
||||
|
||||
- name: Set yarn version
|
||||
run: yarn set version 1.22.18
|
||||
|
6
.github/workflows/publish.yml
vendored
6
.github/workflows/publish.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '18.19.0'
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '18.19.0'
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
@ -107,7 +107,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '18.19.0'
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '18.19.0'
|
||||
|
||||
- name: Set yarn version
|
||||
run: yarn set version 1.22.18
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.14.0'
|
||||
node-version: '18.19.0'
|
||||
|
||||
- name: Set yarn version
|
||||
run: yarn set version 1.22.18
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,6 +23,7 @@ yarn-error.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.code-workspace
|
||||
|
||||
#Electron-builder output
|
||||
/dist_electron
|
||||
|
@ -1,3 +1,5 @@
|
||||
**/types.ts
|
||||
**/dist_electron
|
||||
**/dummy/*.json
|
||||
**/dummy/*.json
|
||||
**/.github/ISSUE_TEMPLATE/*.yml
|
||||
**/patches/v0_21_0/*
|
11
README.md
11
README.md
@ -1,10 +1,3 @@
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Frappe in search of a maintainer for Frappe Books. For more details check
|
||||
> this issue: [#775](https://github.com/frappe/books/issues/775)
|
||||
|
||||
---
|
||||
|
||||
<div align="center" markdown="1">
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/29507195/207267672-d422db6c-d89a-4bbe-9822-468a55c15053.png" alt="Frappe Books logo" width="384"/>
|
||||
@ -147,6 +140,10 @@ If you want to contribute code then you can fork this repo, make changes and rai
|
||||
- [GitHub Discussions](https://github.com/frappe/books/discussions): Used for discussions around a specific topic.
|
||||
- [Frappe Books Blog](https://tech.frappebooks.com/): Sporadically updated dev blog regarding the development of this project.
|
||||
|
||||
## Maintainers
|
||||
|
||||
Frappe Books is currently being maintained by [Mildred Ki'Lya](https://github.com/mildred) and [Isaac-GC](https://github.com/Isaac-GC).
|
||||
|
||||
## Translation Contributors
|
||||
|
||||
| Language | Contributors |
|
||||
|
@ -8,8 +8,10 @@ import {
|
||||
import { ModelNameEnum } from '../../models/types';
|
||||
import DatabaseCore from './core';
|
||||
import { BespokeFunction } from './types';
|
||||
import { DateTime } from 'luxon';
|
||||
import { DocItem, ReturnDocItem } from 'models/inventory/types';
|
||||
import { safeParseFloat } from 'utils/index';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class BespokeQueries {
|
||||
[key: string]: BespokeFunction;
|
||||
@ -390,4 +392,59 @@ export class BespokeQueries {
|
||||
}
|
||||
return returnBalanceItems;
|
||||
}
|
||||
|
||||
static async getPOSTransactedAmount(
|
||||
db: DatabaseCore,
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
lastShiftClosingDate?: Date
|
||||
): Promise<Record<string, Money> | undefined> {
|
||||
const sinvNamesQuery = db.knex!(ModelNameEnum.SalesInvoice)
|
||||
.select('name')
|
||||
.where('isPOS', true)
|
||||
.andWhereBetween('date', [
|
||||
DateTime.fromJSDate(fromDate).toSQLDate(),
|
||||
DateTime.fromJSDate(toDate).toSQLDate(),
|
||||
]);
|
||||
|
||||
if (lastShiftClosingDate) {
|
||||
sinvNamesQuery.andWhere(
|
||||
'created',
|
||||
'>',
|
||||
DateTime.fromJSDate(lastShiftClosingDate).toUTC().toString()
|
||||
);
|
||||
}
|
||||
|
||||
const sinvNames = (await sinvNamesQuery).map(
|
||||
(row: { name: string }) => row.name
|
||||
);
|
||||
|
||||
if (!sinvNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentEntryNames: string[] = (
|
||||
await db.knex!(ModelNameEnum.PaymentFor)
|
||||
.select('parent')
|
||||
.whereIn('referenceName', sinvNames)
|
||||
).map((doc: { parent: string }) => doc.parent);
|
||||
|
||||
const groupedAmounts = (await db.knex!(ModelNameEnum.Payment)
|
||||
.select('paymentMethod')
|
||||
.whereIn('name', paymentEntryNames)
|
||||
.groupBy('paymentMethod')
|
||||
.sum({ amount: 'amount' })) as { paymentMethod: string; amount: Money }[];
|
||||
|
||||
const transactedAmounts = {} as { [paymentMethod: string]: Money };
|
||||
|
||||
if (!groupedAmounts) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of groupedAmounts) {
|
||||
transactedAmounts[row.paymentMethod] = row.amount;
|
||||
}
|
||||
|
||||
return transactedAmounts;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import createInventoryNumberSeries from './createInventoryNumberSeries';
|
||||
import fixRoundOffAccount from './fixRoundOffAccount';
|
||||
import testPatch from './testPatch';
|
||||
import updateSchemas from './updateSchemas';
|
||||
import setPaymentReferenceType from './setPaymentReferenceType';
|
||||
import fixLedgerDateTime from './v0_21_0/fixLedgerDateTime';
|
||||
|
||||
export default [
|
||||
{ name: 'testPatch', version: '0.5.0-beta.0', patch: testPatch },
|
||||
@ -28,4 +30,14 @@ export default [
|
||||
version: '0.6.6-beta.0',
|
||||
patch: createInventoryNumberSeries,
|
||||
},
|
||||
{
|
||||
name: 'setPaymentReferenceType',
|
||||
version: '0.20.1',
|
||||
patch: setPaymentReferenceType,
|
||||
},
|
||||
{
|
||||
name: 'fixLedgerDateTime',
|
||||
version: '0.21.2',
|
||||
patch: fixLedgerDateTime,
|
||||
},
|
||||
] as Patch[];
|
||||
|
12
backend/patches/setPaymentReferenceType.ts
Normal file
12
backend/patches/setPaymentReferenceType.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { DatabaseManager } from '../database/manager';
|
||||
|
||||
async function execute(dm: DatabaseManager) {
|
||||
await dm.db!.knex!('Payment')
|
||||
.where({ referenceType: null, paymentType: 'Pay' })
|
||||
.update({ referenceType: 'PurchaseInvoice' });
|
||||
await dm.db!.knex!('Payment')
|
||||
.where({ referenceType: null, paymentType: 'Receive' })
|
||||
.update({ referenceType: 'SalesInvoice' });
|
||||
}
|
||||
|
||||
export default { execute, beforeMigrate: true };
|
@ -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) {
|
||||
|
40
backend/patches/v0_21_0/fixLedgerDateTime.ts
Normal file
40
backend/patches/v0_21_0/fixLedgerDateTime.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { DatabaseManager } from '../../database/manager';
|
||||
|
||||
/* eslint-disable */
|
||||
async function execute(dm: DatabaseManager) {
|
||||
|
||||
const sourceTables = [
|
||||
"PurchaseInvoice",
|
||||
"SalesInvoice",
|
||||
"JournalEntry",
|
||||
"Payment",
|
||||
"StockMovement",
|
||||
"StockTransfer"
|
||||
];
|
||||
|
||||
await dm.db!.knex!('AccountingLedgerEntry')
|
||||
.select('name', 'date', 'referenceName')
|
||||
.then((trx: Array<{name: string; date: Date; referenceName: string;}> ) => {
|
||||
trx.forEach(async entry => {
|
||||
|
||||
sourceTables.forEach(async table => {
|
||||
await dm.db!.knex!
|
||||
.select('name','date')
|
||||
.from(table)
|
||||
.where({ name: entry['referenceName'] })
|
||||
.then(async (resp: Array<{name: string; date: Date;}>) => {
|
||||
if (resp.length !== 0) {
|
||||
|
||||
const dateTimeValue = new Date(resp[0]['date']);
|
||||
await dm.db!.knex!('AccountingLedgerEntry')
|
||||
.where({ name: entry['name'] })
|
||||
.update({ date: dateTimeValue.toISOString() });
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default { execute, beforeMigrate: true };
|
||||
/* eslint-enable */
|
@ -8,6 +8,7 @@ import * as vite from 'vite';
|
||||
import { getMainProcessCommonConfig } from './helpers.mjs';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import frappeBooksConfig from '../../electron-builder-config.mjs';
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.join(dirname, '..', '..');
|
||||
@ -153,12 +154,8 @@ async function packageApp() {
|
||||
delete builderArgs[opt];
|
||||
}
|
||||
|
||||
const buildOptions = {
|
||||
config: {
|
||||
directories: { output: packageDirPath, app: buildDirPath },
|
||||
files: ['**'],
|
||||
extends: null,
|
||||
},
|
||||
let buildOptions = {
|
||||
config: frappeBooksConfig,
|
||||
...builderArgs,
|
||||
};
|
||||
|
||||
|
69
electron-builder-config.mjs
Normal file
69
electron-builder-config.mjs
Normal file
@ -0,0 +1,69 @@
|
||||
// App is tagged with a .mjs extension to allow
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* electron-builder doesn't look for the APPLE_TEAM_ID environment variable for some reason.
|
||||
* This workaround allows an environment variable to be added to the electron-builder.yml config
|
||||
* collection. See: https://github.com/electron-userland/electron-builder/issues/7812
|
||||
*/
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// const root = path.join(dirname, '..', '..');
|
||||
const root = dirname; // redundant, but is meant to keep with the previous line
|
||||
const buildDirPath = path.join(root, 'dist_electron', 'build');
|
||||
const packageDirPath = path.join(root, 'dist_electron', 'bundled');
|
||||
|
||||
const frappeBooksConfig = {
|
||||
productName: 'Frappe Books',
|
||||
appId: 'io.frappe.books',
|
||||
asarUnpack: '**/*.node',
|
||||
extraResources: [
|
||||
{ from: 'log_creds.txt', to: '../creds/log_creds.txt' },
|
||||
{ from: 'translations', to: '../translations' },
|
||||
{ from: 'templates', to: '../templates' },
|
||||
],
|
||||
files: '**',
|
||||
extends: null,
|
||||
directories: {
|
||||
output: packageDirPath,
|
||||
app: buildDirPath,
|
||||
},
|
||||
mac: {
|
||||
type: 'distribution',
|
||||
category: 'public.app-category.finance',
|
||||
icon: 'build/icon.icns',
|
||||
notarize: {
|
||||
teamId: process.env.APPLE_TEAM_ID || '',
|
||||
},
|
||||
hardenedRuntime: true,
|
||||
gatekeeperAssess: false,
|
||||
darkModeSupport: false,
|
||||
entitlements: 'build/entitlements.mac.plist',
|
||||
entitlementsInherit: 'build/entitlements.mac.plist',
|
||||
publish: ['github'],
|
||||
},
|
||||
win: {
|
||||
publisherName: 'Frappe Technologies Pvt. Ltd.',
|
||||
signDlls: true,
|
||||
icon: 'build/icon.ico',
|
||||
publish: ['github'],
|
||||
target: ['nsis', 'portable'],
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
perMachine: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
installerIcon: 'build/installericon.ico',
|
||||
uninstallerIcon: 'build/uninstallericon.ico',
|
||||
publish: ['github'],
|
||||
},
|
||||
linux: {
|
||||
icon: 'build/icons',
|
||||
category: 'Finance',
|
||||
publish: ['github'],
|
||||
target: ['deb', 'AppImage', 'rpm'],
|
||||
},
|
||||
};
|
||||
|
||||
export default frappeBooksConfig;
|
@ -1,48 +1,48 @@
|
||||
productName: Frappe Books
|
||||
appId: io.frappe.books
|
||||
asarUnpack: '**/*.node'
|
||||
extraResources:
|
||||
[
|
||||
{ from: 'log_creds.txt', to: '../creds/log_creds.txt' },
|
||||
{ from: 'translations', to: '../translations' },
|
||||
{ from: 'templates', to: '../templates' },
|
||||
]
|
||||
mac:
|
||||
type: distribution
|
||||
category: public.app-category.finance
|
||||
icon: build/icon.icns
|
||||
notarize:
|
||||
appBundleId: io.frappe.books
|
||||
hardenedRuntime: true
|
||||
gatekeeperAssess: false
|
||||
darkModeSupport: false
|
||||
entitlements: build/entitlements.mac.plist
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
publish:
|
||||
- github
|
||||
win:
|
||||
publisherName: Frappe Technologies Pvt. Ltd.
|
||||
signDlls: true
|
||||
icon: build/icon.ico
|
||||
publish:
|
||||
- github
|
||||
target:
|
||||
- portable
|
||||
- nsis
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
installerIcon: build/installericon.ico
|
||||
uninstallerIcon: build/uninstallericon.ico
|
||||
publish:
|
||||
- github
|
||||
linux:
|
||||
icon: build/icons
|
||||
category: Finance
|
||||
publish:
|
||||
- github
|
||||
target:
|
||||
- deb
|
||||
- AppImage
|
||||
- rpm
|
||||
productName: Frappe Books
|
||||
appId: io.frappe.books
|
||||
asarUnpack: '**/*.node'
|
||||
extraResources:
|
||||
[
|
||||
{ from: 'log_creds.txt', to: '../creds/log_creds.txt' },
|
||||
{ from: 'translations', to: '../translations' },
|
||||
{ from: 'templates', to: '../templates' },
|
||||
]
|
||||
mac:
|
||||
type: distribution
|
||||
category: public.app-category.finance
|
||||
icon: build/icon.icns
|
||||
# notarize:
|
||||
# appBundleId: io.frappe.books
|
||||
hardenedRuntime: true
|
||||
gatekeeperAssess: false
|
||||
darkModeSupport: false
|
||||
entitlements: build/entitlements.mac.plist
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
publish:
|
||||
- github
|
||||
win:
|
||||
publisherName: Frappe Technologies Pvt. Ltd.
|
||||
signDlls: true
|
||||
icon: build/icon.ico
|
||||
publish:
|
||||
- github
|
||||
target:
|
||||
- portable
|
||||
- nsis
|
||||
nsis:
|
||||
oneClick: false
|
||||
perMachine: false
|
||||
allowToChangeInstallationDirectory: true
|
||||
installerIcon: build/installericon.ico
|
||||
uninstallerIcon: build/uninstallericon.ico
|
||||
publish:
|
||||
- github
|
||||
linux:
|
||||
icon: build/icons
|
||||
category: Finance
|
||||
publish:
|
||||
- github
|
||||
target:
|
||||
- deb
|
||||
- AppImage
|
||||
- rpm
|
@ -27,6 +27,7 @@ import {
|
||||
RawValueMap,
|
||||
} from './types';
|
||||
import { ReturnDocItem } from 'models/inventory/types';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
type FieldMap = Record<string, Record<string, Field>>;
|
||||
|
||||
@ -342,6 +343,19 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
)) as Promise<Record<string, ReturnDocItem> | undefined>;
|
||||
}
|
||||
|
||||
async getPOSTransactedAmount(
|
||||
fromDate: Date,
|
||||
toDate: Date,
|
||||
lastShiftClosingDate?: Date
|
||||
): Promise<Record<string, Money> | undefined> {
|
||||
return (await this.#demux.callBespoke(
|
||||
'getPOSTransactedAmount',
|
||||
fromDate,
|
||||
toDate,
|
||||
lastShiftClosingDate
|
||||
)) as Promise<Record<string, Money> | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal methods
|
||||
*/
|
||||
|
@ -10,6 +10,8 @@ import type { Defaults } from 'models/baseModels/Defaults/Defaults';
|
||||
import type { PrintSettings } from 'models/baseModels/PrintSettings/PrintSettings';
|
||||
import type { InventorySettings } from 'models/inventory/InventorySettings';
|
||||
import type { Misc } from 'models/baseModels/Misc';
|
||||
import type { POSSettings } from 'models/inventory/Point of Sale/POSSettings';
|
||||
import type { POSShift } from 'models/inventory/Point of Sale/POSShift';
|
||||
|
||||
/**
|
||||
* The functions below are used for dynamic evaluation
|
||||
@ -54,6 +56,8 @@ export interface SinglesMap {
|
||||
SystemSettings?: SystemSettings;
|
||||
AccountingSettings?: AccountingSettings;
|
||||
InventorySettings?: InventorySettings;
|
||||
POSSettings?: POSSettings;
|
||||
POSShift?: POSShift;
|
||||
PrintSettings?: PrintSettings;
|
||||
Defaults?: Defaults;
|
||||
Misc?: Misc;
|
||||
|
@ -20,14 +20,12 @@ export async function saveHtmlAsPdf(
|
||||
|
||||
const printWindow = await getInitializedPrintWindow(htmlPath, width, height);
|
||||
const printOptions = {
|
||||
marginsType: 1, // no margin
|
||||
margins: { top: 0, bottom: 0, left: 0, right: 0 }, // equivalent to previous 'marginType: 1'
|
||||
pageSize: {
|
||||
height: height * 10_000, // micrometers
|
||||
width: width * 10_000, // micrometers
|
||||
height: height / 2.54, // Convert from centimeters to inches
|
||||
width: width / 2.54, // Convert from centimeters to inches
|
||||
},
|
||||
printBackground: true,
|
||||
printBackgrounds: true,
|
||||
printSelectionOnly: false,
|
||||
};
|
||||
|
||||
const data = await printWindow.webContents.printToPDF(printOptions);
|
||||
|
@ -61,6 +61,21 @@ export class LedgerPosting {
|
||||
this._validateIsEqual();
|
||||
}
|
||||
|
||||
timezoneDateTimeAdjuster(setDate: string | Date) {
|
||||
const dateTimeValue = new Date(setDate);
|
||||
|
||||
const dtFixedValue = dateTimeValue;
|
||||
const dtMinutes = dtFixedValue.getTimezoneOffset() % 60;
|
||||
const dtHours = (dtFixedValue.getTimezoneOffset() - dtMinutes) / 60;
|
||||
// Forcing the time to always be set to 00:00.000 for locale time
|
||||
dtFixedValue.setHours(0 - dtHours);
|
||||
dtFixedValue.setMinutes(0 - dtMinutes);
|
||||
dtFixedValue.setSeconds(0);
|
||||
dtFixedValue.setMilliseconds(0);
|
||||
|
||||
return dtFixedValue;
|
||||
}
|
||||
|
||||
async makeRoundOffEntry() {
|
||||
const { debit, credit } = this._getTotalDebitAndCredit();
|
||||
const difference = debit.sub(credit);
|
||||
@ -90,12 +105,14 @@ export class LedgerPosting {
|
||||
return map[account];
|
||||
}
|
||||
|
||||
// end ugly timezone fix code
|
||||
|
||||
const ledgerEntry = this.fyo.doc.getNewDoc(
|
||||
ModelNameEnum.AccountingLedgerEntry,
|
||||
{
|
||||
account: account,
|
||||
party: (this.refDoc.party as string) ?? '',
|
||||
date: this.refDoc.date as string | Date,
|
||||
date: this.timezoneDateTimeAdjuster(this.refDoc.date as string | Date),
|
||||
referenceType: this.refDoc.schemaName,
|
||||
referenceName: this.refDoc.name!,
|
||||
reverted: this.reverted,
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { DefaultCashDenominations } from 'models/inventory/Point of Sale/DefaultCashDenominations';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap, HiddenMap } from 'fyo/model/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { PartyRoleEnum } from '../Party/types';
|
||||
|
||||
export class Defaults extends Doc {
|
||||
// Auto Payments
|
||||
@ -12,6 +14,7 @@ export class Defaults extends Doc {
|
||||
purchaseReceiptLocation?: string;
|
||||
|
||||
// Number Series
|
||||
salesQuoteNumberSeries?: string;
|
||||
salesInvoiceNumberSeries?: string;
|
||||
purchaseInvoiceNumberSeries?: string;
|
||||
journalEntryNumberSeries?: string;
|
||||
@ -27,6 +30,7 @@ export class Defaults extends Doc {
|
||||
purchaseReceiptTerms?: string;
|
||||
|
||||
// Print Templates
|
||||
salesQuotePrintTemplate?: string;
|
||||
salesInvoicePrintTemplate?: string;
|
||||
purchaseInvoicePrintTemplate?: string;
|
||||
journalEntryPrintTemplate?: string;
|
||||
@ -35,11 +39,18 @@ export class Defaults extends Doc {
|
||||
purchaseReceiptPrintTemplate?: string;
|
||||
stockMovementPrintTemplate?: string;
|
||||
|
||||
// Point of Sale
|
||||
posCashDenominations?: DefaultCashDenominations[];
|
||||
posCustomer?: string;
|
||||
|
||||
static commonFilters = {
|
||||
// Auto Payments
|
||||
salesPaymentAccount: () => ({ isGroup: false, accountType: 'Cash' }),
|
||||
purchasePaymentAccount: () => ({ isGroup: false, accountType: 'Cash' }),
|
||||
// Number Series
|
||||
salesQuoteNumberSeries: () => ({
|
||||
referenceType: ModelNameEnum.SalesQuote,
|
||||
}),
|
||||
salesInvoiceNumberSeries: () => ({
|
||||
referenceType: ModelNameEnum.SalesInvoice,
|
||||
}),
|
||||
@ -62,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,
|
||||
@ -73,6 +85,7 @@ export class Defaults extends Doc {
|
||||
type: ModelNameEnum.PurchaseReceipt,
|
||||
}),
|
||||
stockMovementPrintTemplate: () => ({ type: ModelNameEnum.StockMovement }),
|
||||
posCustomer: () => ({ role: PartyRoleEnum.Customer }),
|
||||
};
|
||||
|
||||
static filters: FiltersMap = this.commonFilters;
|
||||
@ -82,6 +95,10 @@ export class Defaults extends Doc {
|
||||
return () => !this.fyo.singles.AccountingSettings?.enableInventory;
|
||||
}
|
||||
|
||||
getPointOfSaleHidden() {
|
||||
return () => !this.fyo.singles.InventorySettings?.enablePointOfSale;
|
||||
}
|
||||
|
||||
hidden: HiddenMap = {
|
||||
stockMovementNumberSeries: this.getInventoryHidden(),
|
||||
shipmentNumberSeries: this.getInventoryHidden(),
|
||||
@ -91,6 +108,8 @@ export class Defaults extends Doc {
|
||||
shipmentPrintTemplate: this.getInventoryHidden(),
|
||||
purchaseReceiptPrintTemplate: this.getInventoryHidden(),
|
||||
stockMovementPrintTemplate: this.getInventoryHidden(),
|
||||
posCashDenominations: this.getPointOfSaleHidden(),
|
||||
posCustomer: this.getPointOfSaleHidden(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,4 +124,5 @@ export const numberSeriesDefaultsMap: Record<
|
||||
[ModelNameEnum.StockMovement]: 'stockMovementNumberSeries',
|
||||
[ModelNameEnum.Shipment]: 'shipmentNumberSeries',
|
||||
[ModelNameEnum.PurchaseReceipt]: 'purchaseReceiptNumberSeries',
|
||||
[ModelNameEnum.SalesQuote]: 'salesQuoteNumberSeries',
|
||||
};
|
||||
|
@ -28,6 +28,19 @@ import { TaxSummary } from '../TaxSummary/TaxSummary';
|
||||
import { ReturnDocItem } from 'models/inventory/types';
|
||||
import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/types';
|
||||
|
||||
export type TaxDetail = {
|
||||
account: string;
|
||||
payment_account?: string;
|
||||
rate: number;
|
||||
};
|
||||
|
||||
export type InvoiceTaxItem = {
|
||||
details: TaxDetail;
|
||||
exchangeRate?: number;
|
||||
fullAmount: Money;
|
||||
taxAmount: Money;
|
||||
};
|
||||
|
||||
export abstract class Invoice extends Transactional {
|
||||
_taxes: Record<string, Tax> = {};
|
||||
taxes?: TaxSummary[];
|
||||
@ -58,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() {
|
||||
@ -242,6 +261,38 @@ export abstract class Invoice extends Transactional {
|
||||
return safeParseFloat(exchangeRate.toFixed(2));
|
||||
}
|
||||
|
||||
async getTaxItems(): Promise<InvoiceTaxItem[]> {
|
||||
const taxItems: InvoiceTaxItem[] = [];
|
||||
for (const item of this.items ?? []) {
|
||||
if (!item.tax) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tax = await this.getTax(item.tax);
|
||||
for (const details of (tax.details ?? []) as TaxDetail[]) {
|
||||
let amount = item.amount!;
|
||||
if (
|
||||
this.enableDiscounting &&
|
||||
!this.discountAfterTax &&
|
||||
!item.itemDiscountedTotal?.isZero()
|
||||
) {
|
||||
amount = item.itemDiscountedTotal!;
|
||||
}
|
||||
|
||||
const taxItem: InvoiceTaxItem = {
|
||||
details,
|
||||
exchangeRate: this.exchangeRate ?? 1,
|
||||
fullAmount: amount,
|
||||
taxAmount: amount.mul(details.rate / 100),
|
||||
};
|
||||
|
||||
taxItems.push(taxItem);
|
||||
}
|
||||
}
|
||||
|
||||
return taxItems;
|
||||
}
|
||||
|
||||
async getTaxSummary() {
|
||||
const taxes: Record<
|
||||
string,
|
||||
@ -252,33 +303,16 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
> = {};
|
||||
|
||||
type TaxDetail = { account: string; rate: number };
|
||||
for (const { details, taxAmount } of await this.getTaxItems()) {
|
||||
const account = details.account;
|
||||
|
||||
for (const item of this.items ?? []) {
|
||||
if (!item.tax) {
|
||||
continue;
|
||||
}
|
||||
taxes[account] ??= {
|
||||
account,
|
||||
rate: details.rate,
|
||||
amount: this.fyo.pesa(0),
|
||||
};
|
||||
|
||||
const tax = await this.getTax(item.tax);
|
||||
for (const { account, rate } of (tax.details ?? []) as TaxDetail[]) {
|
||||
taxes[account] ??= {
|
||||
account,
|
||||
rate,
|
||||
amount: this.fyo.pesa(0),
|
||||
};
|
||||
|
||||
let amount = item.amount!;
|
||||
if (
|
||||
this.enableDiscounting &&
|
||||
!this.discountAfterTax &&
|
||||
!item.itemDiscountedTotal?.isZero()
|
||||
) {
|
||||
amount = item.itemDiscountedTotal!;
|
||||
}
|
||||
|
||||
const taxAmount = amount.mul(rate / 100);
|
||||
taxes[account].amount = taxes[account].amount.add(taxAmount);
|
||||
}
|
||||
taxes[account].amount = taxes[account].amount.add(taxAmount);
|
||||
}
|
||||
|
||||
type Summary = typeof taxes[string] & { idx: number };
|
||||
@ -465,7 +499,7 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
|
||||
async _updateIsItemsReturned() {
|
||||
if (!this.isReturn || !this.returnAgainst) {
|
||||
if (!this.isReturn || !this.returnAgainst || this.isQuote) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -487,7 +521,7 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
|
||||
async _validateHasLinkedReturnInvoices() {
|
||||
if (!this.name || this.isReturn) {
|
||||
if (!this.name || this.isReturn || this.isQuote) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -657,7 +691,10 @@ export abstract class Invoice extends Transactional {
|
||||
attachment: () =>
|
||||
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
|
||||
backReference: () => !this.backReference,
|
||||
priceList: () => !this.fyo.singles.AccountingSettings?.enablePriceList,
|
||||
quote: () => !this.quote,
|
||||
priceList: () =>
|
||||
!this.fyo.singles.AccountingSettings?.enablePriceList ||
|
||||
(!this.canEdit && !this.priceList),
|
||||
returnAgainst: () =>
|
||||
(this.isSubmitted || this.isCancelled) && !this.returnAgainst,
|
||||
};
|
||||
|
@ -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() {
|
||||
|
@ -28,10 +28,12 @@ import { Invoice } from '../Invoice/Invoice';
|
||||
import { Party } from '../Party/Party';
|
||||
import { PaymentFor } from '../PaymentFor/PaymentFor';
|
||||
import { PaymentMethod, PaymentType } from './types';
|
||||
import { TaxSummary } from '../TaxSummary/TaxSummary';
|
||||
|
||||
type AccountTypeMap = Record<AccountTypeEnum, string[] | undefined>;
|
||||
|
||||
export class Payment extends Transactional {
|
||||
taxes?: TaxSummary[];
|
||||
party?: string;
|
||||
amount?: Money;
|
||||
writeoff?: Money;
|
||||
@ -221,6 +223,86 @@ export class Payment extends Transactional {
|
||||
);
|
||||
}
|
||||
|
||||
async getTaxSummary() {
|
||||
const taxes: Record<
|
||||
string,
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
account: string;
|
||||
from_account: string;
|
||||
rate: number;
|
||||
amount: Money;
|
||||
}
|
||||
>
|
||||
> = {};
|
||||
|
||||
for (const childDoc of this.for ?? []) {
|
||||
const referenceName = childDoc.referenceName;
|
||||
const referenceType = childDoc.referenceType;
|
||||
|
||||
const refDoc = (await this.fyo.doc.getDoc(
|
||||
childDoc.referenceType!,
|
||||
childDoc.referenceName
|
||||
)) as Invoice;
|
||||
|
||||
if (referenceName && referenceType && !refDoc) {
|
||||
throw new ValidationError(
|
||||
t`${referenceType} of type ${
|
||||
this.fyo.schemaMap?.[referenceType]?.label ?? referenceType
|
||||
} does not exist`
|
||||
);
|
||||
}
|
||||
|
||||
if (!refDoc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const {
|
||||
details,
|
||||
taxAmount,
|
||||
exchangeRate,
|
||||
} of await refDoc.getTaxItems()) {
|
||||
const { account, payment_account } = details;
|
||||
if (!payment_account) {
|
||||
continue;
|
||||
}
|
||||
|
||||
taxes[payment_account] ??= {};
|
||||
taxes[payment_account][account] ??= {
|
||||
account: payment_account,
|
||||
from_account: account,
|
||||
rate: details.rate,
|
||||
amount: this.fyo.pesa(0),
|
||||
};
|
||||
|
||||
taxes[payment_account][account].amount = taxes[payment_account][
|
||||
account
|
||||
].amount.add(taxAmount.mul(exchangeRate ?? 1));
|
||||
}
|
||||
}
|
||||
|
||||
type Summary = typeof taxes[string][string] & { idx: number };
|
||||
const taxArr: Summary[] = [];
|
||||
let idx = 0;
|
||||
for (const payment_account in taxes) {
|
||||
for (const account in taxes[payment_account]) {
|
||||
const tax = taxes[payment_account][account];
|
||||
if (tax.amount.isZero()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
taxArr.push({
|
||||
...tax,
|
||||
idx,
|
||||
});
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return taxArr;
|
||||
}
|
||||
|
||||
async getPosting() {
|
||||
/**
|
||||
* account : From Account
|
||||
@ -244,6 +326,20 @@ export class Payment extends Transactional {
|
||||
await posting.debit(paymentAccount, amount);
|
||||
await posting.credit(account, amount);
|
||||
|
||||
if (this.taxes) {
|
||||
if (this.paymentType === 'Receive') {
|
||||
for (const tax of this.taxes) {
|
||||
await posting.debit(tax.from_account!, tax.amount!);
|
||||
await posting.credit(tax.account!, tax.amount!);
|
||||
}
|
||||
} else if (this.paymentType === 'Pay') {
|
||||
for (const tax of this.taxes) {
|
||||
await posting.credit(tax.from_account!, tax.amount!);
|
||||
await posting.debit(tax.account!, tax.amount!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.applyWriteOffPosting(posting);
|
||||
return posting;
|
||||
}
|
||||
@ -508,7 +604,7 @@ export class Payment extends Transactional {
|
||||
const outstanding = partyDoc.outstandingAmount as Money;
|
||||
|
||||
if (outstanding.isNegative()) {
|
||||
if (this.referenceType === ModelNameEnum.SalesInvoice) {
|
||||
if (this.referenceType === ModelNameEnum.PurchaseInvoice) {
|
||||
return 'Pay';
|
||||
}
|
||||
return 'Receive';
|
||||
@ -546,6 +642,7 @@ export class Payment extends Transactional {
|
||||
return this.for![0].referenceType;
|
||||
},
|
||||
},
|
||||
taxes: { formula: async () => await this.getTaxSummary() },
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
@ -588,6 +685,7 @@ export class Payment extends Transactional {
|
||||
attachment: () =>
|
||||
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
|
||||
for: () => !!((this.isSubmitted || this.isCancelled) && !this.for?.length),
|
||||
taxes: () => !this.taxes?.length,
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
|
@ -55,6 +55,7 @@ export class PrintTemplate extends Doc {
|
||||
|
||||
const models = [
|
||||
ModelNameEnum.SalesInvoice,
|
||||
ModelNameEnum.SalesQuote,
|
||||
ModelNameEnum.PurchaseInvoice,
|
||||
ModelNameEnum.JournalEntry,
|
||||
ModelNameEnum.Payment,
|
||||
|
67
models/baseModels/SalesQuote/SalesQuote.ts
Normal file
67
models/baseModels/SalesQuote/SalesQuote.ts
Normal 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);
|
||||
}
|
||||
}
|
3
models/baseModels/SalesQuoteItem/SalesQuoteItem.ts
Normal file
3
models/baseModels/SalesQuoteItem/SalesQuoteItem.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
|
||||
|
||||
export class SalesQuoteItem extends InvoiceItem {}
|
@ -9,6 +9,7 @@ import { Invoice } from '../Invoice/Invoice';
|
||||
|
||||
export class TaxSummary extends Doc {
|
||||
account?: string;
|
||||
from_account?: string;
|
||||
rate?: number;
|
||||
amount?: Money;
|
||||
parentdoc?: Invoice;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
@ -33,6 +35,12 @@ import { ShipmentItem } from './inventory/ShipmentItem';
|
||||
import { StockLedgerEntry } from './inventory/StockLedgerEntry';
|
||||
import { StockMovement } from './inventory/StockMovement';
|
||||
import { StockMovementItem } from './inventory/StockMovementItem';
|
||||
import { ClosingAmounts } from './inventory/Point of Sale/ClosingAmounts';
|
||||
import { ClosingCash } from './inventory/Point of Sale/ClosingCash';
|
||||
import { OpeningAmounts } from './inventory/Point of Sale/OpeningAmounts';
|
||||
import { OpeningCash } from './inventory/Point of Sale/OpeningCash';
|
||||
import { POSSettings } from './inventory/Point of Sale/POSSettings';
|
||||
import { POSShift } from './inventory/Point of Sale/POSShift';
|
||||
|
||||
export const models = {
|
||||
Account,
|
||||
@ -55,6 +63,8 @@ export const models = {
|
||||
PurchaseInvoiceItem,
|
||||
SalesInvoice,
|
||||
SalesInvoiceItem,
|
||||
SalesQuote,
|
||||
SalesQuoteItem,
|
||||
SerialNumber,
|
||||
SetupWizard,
|
||||
PrintTemplate,
|
||||
@ -70,6 +80,13 @@ export const models = {
|
||||
ShipmentItem,
|
||||
PurchaseReceipt,
|
||||
PurchaseReceiptItem,
|
||||
// POS Models
|
||||
ClosingAmounts,
|
||||
ClosingCash,
|
||||
OpeningAmounts,
|
||||
OpeningCash,
|
||||
POSSettings,
|
||||
POSShift,
|
||||
} as ModelMap;
|
||||
|
||||
export async function getRegionalModels(
|
||||
|
@ -12,6 +12,7 @@ export class InventorySettings extends Doc {
|
||||
enableSerialNumber?: boolean;
|
||||
enableUomConversions?: boolean;
|
||||
enableStockReturns?: boolean;
|
||||
enablePointOfSale?: boolean;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
stockInHand: () => ({
|
||||
@ -44,5 +45,8 @@ export class InventorySettings extends Doc {
|
||||
enableStockReturns: () => {
|
||||
return !!this.enableStockReturns;
|
||||
},
|
||||
enablePointOfSale: () => {
|
||||
return !!this.fyo.singles.POSShift?.isShiftOpen;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
6
models/inventory/Point of Sale/CashDenominations.ts
Normal file
6
models/inventory/Point of Sale/CashDenominations.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export abstract class CashDenominations extends Doc {
|
||||
denomination?: Money;
|
||||
}
|
27
models/inventory/Point of Sale/ClosingAmounts.ts
Normal file
27
models/inventory/Point of Sale/ClosingAmounts.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FormulaMap } from 'fyo/model/types';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class ClosingAmounts extends Doc {
|
||||
closingAmount?: Money;
|
||||
differenceAmount?: Money;
|
||||
expectedAmount?: Money;
|
||||
openingAmount?: Money;
|
||||
paymentMethod?: string;
|
||||
|
||||
formulas: FormulaMap = {
|
||||
differenceAmount: {
|
||||
formula: () => {
|
||||
if (!this.closingAmount) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
if (!this.expectedAmount) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
return this.closingAmount.sub(this.expectedAmount);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
5
models/inventory/Point of Sale/ClosingCash.ts
Normal file
5
models/inventory/Point of Sale/ClosingCash.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { CashDenominations } from './CashDenominations';
|
||||
|
||||
export class ClosingCash extends CashDenominations {
|
||||
count?: number;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { CashDenominations } from './CashDenominations';
|
||||
|
||||
export class DefaultCashDenominations extends CashDenominations {}
|
11
models/inventory/Point of Sale/OpeningAmounts.ts
Normal file
11
models/inventory/Point of Sale/OpeningAmounts.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class OpeningAmounts extends Doc {
|
||||
amount?: Money;
|
||||
paymentMethod?: 'Cash' | 'Transfer';
|
||||
|
||||
get openingCashAmount() {
|
||||
return this.parentdoc?.openingCashAmount as Money;
|
||||
}
|
||||
}
|
5
models/inventory/Point of Sale/OpeningCash.ts
Normal file
5
models/inventory/Point of Sale/OpeningCash.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { CashDenominations } from './CashDenominations';
|
||||
|
||||
export class OpeningCash extends CashDenominations {
|
||||
count?: number;
|
||||
}
|
19
models/inventory/Point of Sale/POSSettings.ts
Normal file
19
models/inventory/Point of Sale/POSSettings.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap } from 'fyo/model/types';
|
||||
import {
|
||||
AccountRootTypeEnum,
|
||||
AccountTypeEnum,
|
||||
} from 'models/baseModels/Account/types';
|
||||
|
||||
export class POSSettings extends Doc {
|
||||
inventory?: string;
|
||||
cashAccount?: string;
|
||||
writeOffAccount?: string;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
cashAccount: () => ({
|
||||
rootType: AccountRootTypeEnum.Asset,
|
||||
accountType: AccountTypeEnum.Cash,
|
||||
}),
|
||||
};
|
||||
}
|
61
models/inventory/Point of Sale/POSShift.ts
Normal file
61
models/inventory/Point of Sale/POSShift.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { ClosingAmounts } from './ClosingAmounts';
|
||||
import { ClosingCash } from './ClosingCash';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { OpeningAmounts } from './OpeningAmounts';
|
||||
import { OpeningCash } from './OpeningCash';
|
||||
|
||||
export class POSShift extends Doc {
|
||||
closingAmounts?: ClosingAmounts[];
|
||||
closingCash?: ClosingCash[];
|
||||
closingDate?: Date;
|
||||
isShiftOpen?: boolean;
|
||||
openingAmounts?: OpeningAmounts[];
|
||||
openingCash?: OpeningCash[];
|
||||
openingDate?: Date;
|
||||
|
||||
get openingCashAmount() {
|
||||
if (!this.openingCash) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
let openingAmount = this.fyo.pesa(0);
|
||||
|
||||
this.openingCash.map((row: OpeningCash) => {
|
||||
const denomination = row.denomination ?? this.fyo.pesa(0);
|
||||
const count = row.count ?? 0;
|
||||
|
||||
const amount = denomination.mul(count);
|
||||
openingAmount = openingAmount.add(amount);
|
||||
});
|
||||
return openingAmount;
|
||||
}
|
||||
|
||||
get closingCashAmount() {
|
||||
if (!this.closingCash) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
let closingAmount = this.fyo.pesa(0);
|
||||
|
||||
this.closingCash.map((row: ClosingCash) => {
|
||||
const denomination = row.denomination ?? this.fyo.pesa(0);
|
||||
const count = row.count ?? 0;
|
||||
|
||||
const amount = denomination.mul(count);
|
||||
closingAmount = closingAmount.add(amount);
|
||||
});
|
||||
return closingAmount;
|
||||
}
|
||||
|
||||
get openingTransferAmount() {
|
||||
if (!this.openingAmounts) {
|
||||
return this.fyo.pesa(0);
|
||||
}
|
||||
|
||||
const transferAmountRow = this.openingAmounts.filter(
|
||||
(row) => row.paymentMethod === 'Transfer'
|
||||
)[0];
|
||||
|
||||
return transferAmountRow.amount ?? this.fyo.pesa(0);
|
||||
}
|
||||
}
|
103
models/inventory/Point of Sale/tests/testPointOfSale.spec.ts
Normal file
103
models/inventory/Point of Sale/tests/testPointOfSale.spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import test from 'tape';
|
||||
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { Payment } from 'models/baseModels/Payment/Payment';
|
||||
import { Money } from 'pesa';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
|
||||
const fyo = getTestFyo();
|
||||
|
||||
setupTestFyo(fyo, __filename);
|
||||
|
||||
const customer = { name: 'Someone', role: 'Both' };
|
||||
const itemMap = {
|
||||
Pen: {
|
||||
name: 'Pen',
|
||||
rate: 700,
|
||||
},
|
||||
Ink: {
|
||||
name: 'Ink',
|
||||
rate: 50,
|
||||
},
|
||||
};
|
||||
|
||||
test('insert test docs', async (t) => {
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Item, itemMap.Pen).sync();
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Item, itemMap.Ink).sync();
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Party, customer).sync();
|
||||
});
|
||||
|
||||
let sinvDocOne: SalesInvoice | undefined;
|
||||
|
||||
test('check pos transacted amount', async (t) => {
|
||||
const transactedAmountBeforeTxn = await fyo.db.getPOSTransactedAmount(
|
||||
new Date('2023-01-01'),
|
||||
new Date('2023-01-02')
|
||||
);
|
||||
|
||||
t.equals(transactedAmountBeforeTxn, undefined);
|
||||
|
||||
sinvDocOne = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
isPOS: true,
|
||||
date: new Date('2023-01-01'),
|
||||
account: 'Debtors',
|
||||
party: customer.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinvDocOne.append('items', {
|
||||
item: itemMap.Pen.name,
|
||||
rate: itemMap.Pen.rate,
|
||||
quantity: 1,
|
||||
});
|
||||
|
||||
await (await sinvDocOne.sync()).submit();
|
||||
const paymentDocOne = sinvDocOne.getPayment() as Payment;
|
||||
|
||||
await paymentDocOne.sync();
|
||||
|
||||
const sinvDocTwo = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
isPOS: true,
|
||||
date: new Date('2023-01-01'),
|
||||
account: 'Debtors',
|
||||
party: customer.name,
|
||||
}) as SalesInvoice;
|
||||
|
||||
await sinvDocTwo.append('items', {
|
||||
item: itemMap.Pen.name,
|
||||
rate: itemMap.Pen.rate,
|
||||
quantity: 1,
|
||||
});
|
||||
|
||||
await (await sinvDocTwo.sync()).submit();
|
||||
const paymentDocTwo = sinvDocTwo.getPayment() as Payment;
|
||||
|
||||
await paymentDocTwo.setMultiple({
|
||||
paymentMethod: 'Transfer',
|
||||
clearanceDate: new Date('2023-01-01'),
|
||||
referenceId: 'xxxxxxxx',
|
||||
});
|
||||
|
||||
await paymentDocTwo.sync();
|
||||
|
||||
const transactedAmountAfterTxn: Record<string, Money> | undefined =
|
||||
await fyo.db.getPOSTransactedAmount(
|
||||
new Date('2023-01-01'),
|
||||
new Date('2023-01-02')
|
||||
);
|
||||
|
||||
t.true(transactedAmountAfterTxn);
|
||||
|
||||
t.equals(
|
||||
transactedAmountAfterTxn?.Cash,
|
||||
sinvDocOne.grandTotal?.float,
|
||||
'transacted cash amount matches'
|
||||
);
|
||||
|
||||
t.equals(
|
||||
transactedAmountAfterTxn?.Transfer,
|
||||
sinvDocTwo.grandTotal?.float,
|
||||
'transacted transfer amount matches'
|
||||
);
|
||||
});
|
||||
|
||||
closeTestFyo(fyo, __filename);
|
@ -27,6 +27,8 @@ export enum ModelNameEnum {
|
||||
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
|
||||
SalesInvoice = 'SalesInvoice',
|
||||
SalesInvoiceItem = 'SalesInvoiceItem',
|
||||
SalesQuote = 'SalesQuote',
|
||||
SalesQuoteItem = 'SalesQuoteItem',
|
||||
SerialNumber = 'SerialNumber',
|
||||
SetupWizard = 'SetupWizard',
|
||||
Tax = 'Tax',
|
||||
@ -45,7 +47,9 @@ export enum ModelNameEnum {
|
||||
PurchaseReceiptItem = 'PurchaseReceiptItem',
|
||||
Location = 'Location',
|
||||
CustomForm = 'CustomForm',
|
||||
CustomField = 'CustomField'
|
||||
CustomField = 'CustomField',
|
||||
POSSettings = 'POSSettings',
|
||||
POSShift = 'POSShift'
|
||||
}
|
||||
|
||||
export type ModelName = keyof typeof ModelNameEnum;
|
||||
|
17
package.json
17
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frappe-books",
|
||||
"version": "0.19.0",
|
||||
"version": "0.21.2",
|
||||
"description": "Simple book-keeping app for everyone",
|
||||
"author": {
|
||||
"name": "Frappe Technologies Pvt. Ltd.",
|
||||
@ -23,7 +23,7 @@
|
||||
"@codemirror/autocomplete": "^6.4.2",
|
||||
"@codemirror/lang-vue": "^0.1.1",
|
||||
"@popperjs/core": "^2.10.2",
|
||||
"better-sqlite3": "^7.5.3",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"core-js": "^3.19.0",
|
||||
"electron-store": "^8.0.1",
|
||||
@ -41,6 +41,7 @@
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@electron/rebuild": "^3.4.1",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@types/assert": "^1.5.6",
|
||||
"@types/better-sqlite3": "^7.6.4",
|
||||
@ -56,11 +57,10 @@
|
||||
"autoprefixer": "^9",
|
||||
"chokidar": "^3.5.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"electron": "18.3.7",
|
||||
"electron-builder": "^24.4.0",
|
||||
"electron": "22.3.27",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"electron-updater": "^5.2.1",
|
||||
"electron-updater": "^6.1.7",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
@ -78,10 +78,13 @@
|
||||
"tsconfig-paths": "^3.14.1",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.6.2",
|
||||
"vite": "^4.3.9",
|
||||
"vite": "^4.5.2",
|
||||
"vue-tsc": "^1.6.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"node-abi": "^3.54.0"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
|
@ -64,12 +64,12 @@ export abstract class AccountReport extends LedgerReport {
|
||||
this._dateRanges = await this._getDateRanges();
|
||||
}
|
||||
|
||||
getRootNode(
|
||||
getRootNodes(
|
||||
rootType: AccountRootType,
|
||||
accountTree: AccountTree
|
||||
): AccountTreeNode | undefined {
|
||||
): AccountTreeNode[] | undefined {
|
||||
const rootNodeList = Object.values(accountTree);
|
||||
return rootNodeList.find((n) => n.rootType === rootType);
|
||||
return rootNodeList.filter((n) => n.rootType === rootType);
|
||||
}
|
||||
|
||||
getEmptyRow(): ReportRow {
|
||||
@ -88,8 +88,11 @@ export abstract class AccountReport extends LedgerReport {
|
||||
};
|
||||
}
|
||||
|
||||
getTotalNode(rootNode: AccountTreeNode, name: string): AccountListNode {
|
||||
const accountTree = { [rootNode.name]: rootNode };
|
||||
getTotalNode(rootNodes: AccountTreeNode[], name: string): AccountListNode {
|
||||
const accountTree: Tree = {};
|
||||
for (const rootNode of rootNodes) {
|
||||
accountTree[rootNode.name] = rootNode;
|
||||
}
|
||||
const leafNodes = getListOfLeafNodes(accountTree) as AccountTreeNode[];
|
||||
|
||||
const totalMap = leafNodes.reduce((acc, node) => {
|
||||
@ -236,6 +239,17 @@ export abstract class AccountReport extends LedgerReport {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fix arythmetic on dates when adding or substracting months. If the
|
||||
// reference date was the last day in month, ensure that the resulting date is
|
||||
// also the last day.
|
||||
_fixMonthsJump(refDate: DateTime, date: DateTime): DateTime {
|
||||
if (refDate.day == refDate.daysInMonth && date.day != date.daysInMonth) {
|
||||
return date.set({ day: date.daysInMonth });
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
async _getDateRanges(): Promise<DateRange[]> {
|
||||
const endpoints = await this._getFromAndToDates();
|
||||
const fromDate = DateTime.fromISO(endpoints.fromDate);
|
||||
@ -252,7 +266,10 @@ export abstract class AccountReport extends LedgerReport {
|
||||
|
||||
const months: number = monthsMap[this.periodicity];
|
||||
const dateRanges: DateRange[] = [
|
||||
{ toDate, fromDate: toDate.minus({ months }) },
|
||||
{
|
||||
toDate,
|
||||
fromDate: this._fixMonthsJump(toDate, toDate.minus({ months })),
|
||||
},
|
||||
];
|
||||
|
||||
let count = this.count ?? 1;
|
||||
@ -264,7 +281,10 @@ export abstract class AccountReport extends LedgerReport {
|
||||
const lastRange = dateRanges.at(-1)!;
|
||||
dateRanges.push({
|
||||
toDate: lastRange.fromDate,
|
||||
fromDate: lastRange.fromDate.minus({ months }),
|
||||
fromDate: this._fixMonthsJump(
|
||||
toDate,
|
||||
lastRange.fromDate.minus({ months })
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@ -445,14 +465,15 @@ export async function getFiscalEndpoints(
|
||||
|
||||
const fromDate = [
|
||||
fromYear,
|
||||
fys.toISOString().split('T')[0].split('-').slice(1),
|
||||
]
|
||||
.flat()
|
||||
.join('-');
|
||||
(fys.getMonth() + 1).toString().padStart(2, '0'),
|
||||
fys.getDate().toString().padStart(2, '0'),
|
||||
].join('-');
|
||||
|
||||
const toDate = [toYear, fye.toISOString().split('T')[0].split('-').slice(1)]
|
||||
.flat()
|
||||
.join('-');
|
||||
const toDate = [
|
||||
toYear,
|
||||
(fye.getMonth() + 1).toString().padStart(2, '0'),
|
||||
fye.getDate().toString().padStart(2, '0'),
|
||||
].join('-');
|
||||
|
||||
return { fromDate, toDate };
|
||||
}
|
||||
@ -573,15 +594,17 @@ function getPrunedChildren(children: AccountTreeNode[]): AccountTreeNode[] {
|
||||
});
|
||||
}
|
||||
|
||||
export function convertAccountRootNodeToAccountList(
|
||||
rootNode: AccountTreeNode
|
||||
export function convertAccountRootNodesToAccountList(
|
||||
rootNodes: AccountTreeNode[]
|
||||
): AccountList {
|
||||
if (!rootNode) {
|
||||
if (!rootNodes || rootNodes.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const accountList: AccountList = [];
|
||||
pushToAccountList(rootNode, accountList, 0);
|
||||
for (const rootNode of rootNodes) {
|
||||
pushToAccountList(rootNode, accountList, 0);
|
||||
}
|
||||
return accountList;
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
} from 'models/baseModels/Account/types';
|
||||
import {
|
||||
AccountReport,
|
||||
convertAccountRootNodeToAccountList,
|
||||
convertAccountRootNodesToAccountList,
|
||||
} from 'reports/AccountReport';
|
||||
import { ReportData, RootTypeRow } from 'reports/types';
|
||||
import { getMapFromList } from 'utils';
|
||||
@ -44,15 +44,15 @@ export class BalanceSheet extends AccountReport {
|
||||
|
||||
const rootTypeRows: RootTypeRow[] = this.rootTypes
|
||||
.map((rootType) => {
|
||||
const rootNode = this.getRootNode(rootType, accountTree)!;
|
||||
const rootList = convertAccountRootNodeToAccountList(rootNode);
|
||||
const rootNodes = this.getRootNodes(rootType, accountTree)!;
|
||||
const rootList = convertAccountRootNodesToAccountList(rootNodes);
|
||||
return {
|
||||
rootType,
|
||||
rootNode,
|
||||
rootNodes,
|
||||
rows: this.getReportRowsFromAccountList(rootList),
|
||||
};
|
||||
})
|
||||
.filter((row) => !!row.rootNode);
|
||||
.filter((row) => !!row.rootNodes.length);
|
||||
|
||||
this.reportData = this.getReportDataFromRows(
|
||||
getMapFromList(rootTypeRows, 'rootType')
|
||||
@ -88,8 +88,8 @@ export class BalanceSheet extends AccountReport {
|
||||
|
||||
reportData.push(...row.rows);
|
||||
|
||||
if (row.rootNode) {
|
||||
const totalNode = this.getTotalNode(row.rootNode, totalName);
|
||||
if (row.rootNodes.length) {
|
||||
const totalNode = this.getTotalNode(row.rootNodes, totalName);
|
||||
const totalRow = this.getRowFromAccountListNode(totalNode);
|
||||
reportData.push(totalRow);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
} from 'models/baseModels/Account/types';
|
||||
import {
|
||||
AccountReport,
|
||||
convertAccountRootNodeToAccountList,
|
||||
convertAccountRootNodesToAccountList,
|
||||
} from 'reports/AccountReport';
|
||||
import {
|
||||
AccountListNode,
|
||||
@ -45,28 +45,28 @@ export class ProfitAndLoss extends AccountReport {
|
||||
/**
|
||||
* Income Rows
|
||||
*/
|
||||
const incomeRoot = this.getRootNode(
|
||||
const incomeRoots = this.getRootNodes(
|
||||
AccountRootTypeEnum.Income,
|
||||
accountTree
|
||||
)!;
|
||||
const incomeList = convertAccountRootNodeToAccountList(incomeRoot);
|
||||
const incomeList = convertAccountRootNodesToAccountList(incomeRoots);
|
||||
const incomeRows = this.getReportRowsFromAccountList(incomeList);
|
||||
|
||||
/**
|
||||
* Expense Rows
|
||||
*/
|
||||
const expenseRoot = this.getRootNode(
|
||||
const expenseRoots = this.getRootNodes(
|
||||
AccountRootTypeEnum.Expense,
|
||||
accountTree
|
||||
)!;
|
||||
const expenseList = convertAccountRootNodeToAccountList(expenseRoot);
|
||||
const expenseList = convertAccountRootNodesToAccountList(expenseRoots);
|
||||
const expenseRows = this.getReportRowsFromAccountList(expenseList);
|
||||
|
||||
this.reportData = this.getReportDataFromRows(
|
||||
incomeRows,
|
||||
expenseRows,
|
||||
incomeRoot,
|
||||
expenseRoot
|
||||
incomeRoots,
|
||||
expenseRoots
|
||||
);
|
||||
this.loading = false;
|
||||
}
|
||||
@ -74,43 +74,57 @@ export class ProfitAndLoss extends AccountReport {
|
||||
getReportDataFromRows(
|
||||
incomeRows: ReportData,
|
||||
expenseRows: ReportData,
|
||||
incomeRoot: AccountTreeNode | undefined,
|
||||
expenseRoot: AccountTreeNode | undefined
|
||||
incomeRoots: AccountTreeNode[] | undefined,
|
||||
expenseRoots: AccountTreeNode[] | undefined
|
||||
): ReportData {
|
||||
if (incomeRoot && !expenseRoot) {
|
||||
if (
|
||||
incomeRoots &&
|
||||
incomeRoots.length &&
|
||||
!expenseRoots &&
|
||||
!expenseRoots.length
|
||||
) {
|
||||
return this.getIncomeOrExpenseRows(
|
||||
incomeRoot,
|
||||
incomeRoots,
|
||||
incomeRows,
|
||||
t`Total Income (Credit)`
|
||||
);
|
||||
}
|
||||
|
||||
if (expenseRoot && !incomeRoot) {
|
||||
if (
|
||||
expenseRoots &&
|
||||
expenseRoots.length &&
|
||||
(!incomeRoots || !incomeRoots.length)
|
||||
) {
|
||||
return this.getIncomeOrExpenseRows(
|
||||
expenseRoot,
|
||||
expenseRoots,
|
||||
expenseRows,
|
||||
t`Total Income (Credit)`
|
||||
);
|
||||
}
|
||||
|
||||
if (!incomeRoot || !expenseRoot) {
|
||||
if (
|
||||
!incomeRoots ||
|
||||
!incomeRoots.length ||
|
||||
!expenseRoots ||
|
||||
!expenseRoots.length
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.getIncomeAndExpenseRows(
|
||||
incomeRows,
|
||||
expenseRows,
|
||||
incomeRoot,
|
||||
expenseRoot
|
||||
incomeRoots,
|
||||
expenseRoots
|
||||
);
|
||||
}
|
||||
|
||||
getIncomeOrExpenseRows(
|
||||
root: AccountTreeNode,
|
||||
roots: AccountTreeNode[],
|
||||
rows: ReportData,
|
||||
totalRowName: string
|
||||
): ReportData {
|
||||
const total = this.getTotalNode(root, totalRowName);
|
||||
const total = this.getTotalNode(roots, totalRowName);
|
||||
const totalRow = this.getRowFromAccountListNode(total);
|
||||
|
||||
return [rows, totalRow].flat();
|
||||
@ -119,14 +133,17 @@ export class ProfitAndLoss extends AccountReport {
|
||||
getIncomeAndExpenseRows(
|
||||
incomeRows: ReportData,
|
||||
expenseRows: ReportData,
|
||||
incomeRoot: AccountTreeNode,
|
||||
expenseRoot: AccountTreeNode
|
||||
incomeRoots: AccountTreeNode[],
|
||||
expenseRoots: AccountTreeNode[]
|
||||
) {
|
||||
const totalIncome = this.getTotalNode(incomeRoot, t`Total Income (Credit)`);
|
||||
const totalIncome = this.getTotalNode(
|
||||
incomeRoots,
|
||||
t`Total Income (Credit)`
|
||||
);
|
||||
const totalIncomeRow = this.getRowFromAccountListNode(totalIncome);
|
||||
|
||||
const totalExpense = this.getTotalNode(
|
||||
expenseRoot,
|
||||
expenseRoots,
|
||||
t`Total Expense (Debit)`
|
||||
);
|
||||
const totalExpenseRow = this.getRowFromAccountListNode(totalExpense);
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
AccountReport,
|
||||
ACC_BAL_WIDTH,
|
||||
ACC_NAME_WIDTH,
|
||||
convertAccountRootNodeToAccountList,
|
||||
convertAccountRootNodesToAccountList,
|
||||
getFiscalEndpoints,
|
||||
} from 'reports/AccountReport';
|
||||
import {
|
||||
@ -65,15 +65,15 @@ export class TrialBalance extends AccountReport {
|
||||
|
||||
const rootTypeRows: RootTypeRow[] = this.rootTypes
|
||||
.map((rootType) => {
|
||||
const rootNode = this.getRootNode(rootType, accountTree)!;
|
||||
const rootList = convertAccountRootNodeToAccountList(rootNode);
|
||||
const rootNodes = this.getRootNodes(rootType, accountTree)!;
|
||||
const rootList = convertAccountRootNodesToAccountList(rootNodes);
|
||||
return {
|
||||
rootType,
|
||||
rootNode,
|
||||
rootNodes,
|
||||
rows: this.getReportRowsFromAccountList(rootList),
|
||||
};
|
||||
})
|
||||
.filter((row) => !!row.rootNode);
|
||||
.filter((row) => !!(row.rootNodes && row.rootNodes.length));
|
||||
|
||||
this.reportData = await this.getReportDataFromRows(rootTypeRows);
|
||||
this.loading = false;
|
||||
|
@ -107,6 +107,6 @@ export type Tree = Record<string, TreeNode>;
|
||||
|
||||
export type RootTypeRow = {
|
||||
rootType: AccountRootType;
|
||||
rootNode: AccountTreeNode;
|
||||
rootNodes: AccountTreeNode[];
|
||||
rows: ReportData;
|
||||
};
|
@ -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",
|
||||
@ -164,6 +179,21 @@
|
||||
"fieldtype": "Link",
|
||||
"target": "PrintTemplate",
|
||||
"section": "Print Templates"
|
||||
},
|
||||
{
|
||||
"fieldname": "posCustomer",
|
||||
"label": "POS Customer",
|
||||
"fieldtype": "Link",
|
||||
"target": "Party",
|
||||
"create": true,
|
||||
"section": "Point of Sale"
|
||||
},
|
||||
{
|
||||
"fieldname": "posCashDenominations",
|
||||
"label": "Cash Denominations",
|
||||
"fieldtype": "Table",
|
||||
"target": "DefaultCashDenominations",
|
||||
"section": "Point of Sale"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -35,6 +35,10 @@
|
||||
"value": "SalesInvoice",
|
||||
"label": "Sales Invoice"
|
||||
},
|
||||
{
|
||||
"value": "SalesQuote",
|
||||
"label": "Sales Quote"
|
||||
},
|
||||
{
|
||||
"value": "PurchaseInvoice",
|
||||
"label": "Purchase Invoice"
|
||||
|
@ -141,6 +141,14 @@
|
||||
"computed": true,
|
||||
"section": "Amounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxes",
|
||||
"label": "Taxes",
|
||||
"fieldtype": "Table",
|
||||
"target": "TaxSummary",
|
||||
"readOnly": true,
|
||||
"section": "Amounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "for",
|
||||
"label": "Payment Reference",
|
||||
@ -171,8 +179,7 @@
|
||||
"label": "Purchase"
|
||||
}
|
||||
],
|
||||
"hidden": true,
|
||||
"required": true
|
||||
"hidden": true
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
|
@ -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",
|
||||
@ -61,6 +69,12 @@
|
||||
"target": "SalesInvoice",
|
||||
"label": "Return Against",
|
||||
"section": "References"
|
||||
},
|
||||
{
|
||||
"fieldname": "isPOS",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"hidden": true
|
||||
}
|
||||
],
|
||||
"keywordFields": ["name", "party"]
|
||||
|
46
schemas/app/SalesQuote.json
Normal file
46
schemas/app/SalesQuote.json
Normal 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"
|
||||
]
|
||||
}
|
5
schemas/app/SalesQuoteItem.json
Normal file
5
schemas/app/SalesQuoteItem.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "SalesQuoteItem",
|
||||
"label": "Sales Quote Item",
|
||||
"extends": "InvoiceItem"
|
||||
}
|
@ -6,12 +6,20 @@
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "account",
|
||||
"label": "Tax Account",
|
||||
"label": "Tax Invoice Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"create": true,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_account",
|
||||
"label": "Tax Payment Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"create": true,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"label": "Rate",
|
||||
@ -20,5 +28,5 @@
|
||||
"placeholder": "0%"
|
||||
}
|
||||
],
|
||||
"tableFields": ["account", "rate"]
|
||||
"tableFields": ["account", "payment_account", "rate"]
|
||||
}
|
||||
|
@ -10,6 +10,14 @@
|
||||
"target": "Account",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "from_account",
|
||||
"label": "Tax Invoice Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"required": false,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"label": "Tax Rate",
|
||||
|
@ -63,6 +63,13 @@
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "enablePointOfSale",
|
||||
"label": "Enable Point of Sale",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
14
schemas/app/inventory/Point of Sale/CashDenominations.json
Normal file
14
schemas/app/inventory/Point of Sale/CashDenominations.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "CashDenominations",
|
||||
"label": "Cash Denominations",
|
||||
"isAbstract": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "denomination",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Denomination",
|
||||
"placeholder": "Denomination",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
42
schemas/app/inventory/Point of Sale/ClosingAmounts.json
Normal file
42
schemas/app/inventory/Point of Sale/ClosingAmounts.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "ClosingAmounts",
|
||||
"label": "Closing Amount",
|
||||
"isChild": true,
|
||||
"extends": "POSShiftAmounts",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "openingAmount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Opening Amount",
|
||||
"placeholder": "Opening Amount",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "closingAmount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Closing Amount",
|
||||
"placeholder": "Closing Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "expectedAmount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Expected Amount",
|
||||
"placeholder": "Expected Amount",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "differenceAmount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Difference Amount",
|
||||
"placeholder": "Difference Amount",
|
||||
"readOnly": true
|
||||
}
|
||||
],
|
||||
"tableFields": [
|
||||
"paymentMethod",
|
||||
"openingAmount",
|
||||
"closingAmount",
|
||||
"expectedAmount",
|
||||
"differenceAmount"
|
||||
]
|
||||
}
|
17
schemas/app/inventory/Point of Sale/ClosingCash.json
Normal file
17
schemas/app/inventory/Point of Sale/ClosingCash.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ClosingCash",
|
||||
"label": "Closing Cash In Denominations",
|
||||
"isChild": true,
|
||||
"extends": "CashDenominations",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "count",
|
||||
"label": "Count",
|
||||
"placeholder": "Count",
|
||||
"fieldtype": "Int",
|
||||
"default": 0,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tableFields": ["denomination", "count"]
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "DefaultCashDenominations",
|
||||
"label": "Default Cash Denominations",
|
||||
"isChild": true,
|
||||
"extends": "CashDenominations",
|
||||
"tableFields": ["denomination"]
|
||||
}
|
15
schemas/app/inventory/Point of Sale/OpeningAmounts.json
Normal file
15
schemas/app/inventory/Point of Sale/OpeningAmounts.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "OpeningAmounts",
|
||||
"label": "Opening Amount",
|
||||
"isChild": true,
|
||||
"extends": "POSShiftAmounts",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"label": "Amount",
|
||||
"fieldtype": "Currency",
|
||||
"section": "Defaults"
|
||||
}
|
||||
],
|
||||
"tableFields": ["paymentMethod", "amount"]
|
||||
}
|
17
schemas/app/inventory/Point of Sale/OpeningCash.json
Normal file
17
schemas/app/inventory/Point of Sale/OpeningCash.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "OpeningCash",
|
||||
"label": "Opening Cash In Denominations",
|
||||
"isChild": true,
|
||||
"extends": "CashDenominations",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "count",
|
||||
"label": "Count",
|
||||
"placeholder": "Count",
|
||||
"fieldtype": "Int",
|
||||
"default": 0,
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tableFields": ["denomination", "count"]
|
||||
}
|
36
schemas/app/inventory/Point of Sale/POSSettings.json
Normal file
36
schemas/app/inventory/Point of Sale/POSSettings.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "POSSettings",
|
||||
"label": "POS Settings",
|
||||
"isSingle": true,
|
||||
"isChild": false,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "inventory",
|
||||
"label": "Inventory",
|
||||
"fieldtype": "Link",
|
||||
"target": "Location",
|
||||
"create": true,
|
||||
"default": "Stores",
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "cashAccount",
|
||||
"label": "Counter Cash Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"default": "Cash In Hand",
|
||||
"required": true,
|
||||
"create": true,
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "writeOffAccount",
|
||||
"label": "Write Off Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"create": true,
|
||||
"default": "Write Off",
|
||||
"section": "Default"
|
||||
}
|
||||
]
|
||||
}
|
43
schemas/app/inventory/Point of Sale/POSShift.json
Normal file
43
schemas/app/inventory/Point of Sale/POSShift.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "POSShift",
|
||||
"isSingle": true,
|
||||
"isChild": false,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "isShiftOpen",
|
||||
"label": "Is POS Shift Open",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"fieldname": "openingDate",
|
||||
"label": "Opening Date",
|
||||
"fieldtype": "Datetime"
|
||||
},
|
||||
{
|
||||
"fieldname": "closingDate",
|
||||
"label": "Closing Date",
|
||||
"fieldtype": "Datetime"
|
||||
},
|
||||
{
|
||||
"fieldname": "openingCash",
|
||||
"fieldtype": "Table",
|
||||
"target": "OpeningCash"
|
||||
},
|
||||
{
|
||||
"fieldname": "closingCash",
|
||||
"fieldtype": "Table",
|
||||
"target": "ClosingCash"
|
||||
},
|
||||
{
|
||||
"fieldname": "openingAmounts",
|
||||
"fieldtype": "Table",
|
||||
"target": "OpeningAmounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "closingAmounts",
|
||||
"fieldtype": "Table",
|
||||
"target": "ClosingAmounts"
|
||||
}
|
||||
]
|
||||
}
|
25
schemas/app/inventory/Point of Sale/POSShiftAmounts.json
Normal file
25
schemas/app/inventory/Point of Sale/POSShiftAmounts.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "POSShiftAmounts",
|
||||
"label": "POS Shift Amount",
|
||||
"isChild": true,
|
||||
"isAbstract": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "paymentMethod",
|
||||
"label": "Payment Method",
|
||||
"placeholder": "Payment Method",
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
{
|
||||
"value": "Cash",
|
||||
"label": "Cash"
|
||||
},
|
||||
{
|
||||
"value": "Transfer",
|
||||
"label": "Transfer"
|
||||
}
|
||||
],
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
21
schemas/regional/ch/AccountingSettings.json
Normal file
21
schemas/regional/ch/AccountingSettings.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "AccountingSettings",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "taxId",
|
||||
"label": "Tax ID",
|
||||
"fieldtype": "Data",
|
||||
"placeholder": "CHE-123.456.789",
|
||||
"section": "Default"
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
"fullname",
|
||||
"email",
|
||||
"companyName",
|
||||
"country",
|
||||
"fiscalYearStart",
|
||||
"fiscalYearEnd",
|
||||
"taxId"
|
||||
]
|
||||
}
|
4
schemas/regional/ch/index.ts
Normal file
4
schemas/regional/ch/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { SchemaStub } from '../../types';
|
||||
import AccountingSettings from './AccountingSettings.json';
|
||||
|
||||
export default [AccountingSettings] as SchemaStub[];
|
@ -1,7 +1,11 @@
|
||||
import { SchemaStub } from 'schemas/types';
|
||||
import IndianSchemas from './in';
|
||||
import SwissSchemas from './ch';
|
||||
|
||||
/**
|
||||
* Regional Schemas are exported by country code.
|
||||
*/
|
||||
export default { in: IndianSchemas } as Record<string, SchemaStub[]>;
|
||||
export default { in: IndianSchemas, ch: SwissSchemas } as Record<
|
||||
string,
|
||||
SchemaStub[]
|
||||
>;
|
||||
|
@ -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';
|
||||
@ -52,6 +54,15 @@ import base from './meta/base.json';
|
||||
import child from './meta/child.json';
|
||||
import submittable from './meta/submittable.json';
|
||||
import tree from './meta/tree.json';
|
||||
import CashDenominations from './app/inventory/Point of Sale/CashDenominations.json';
|
||||
import ClosingAmounts from './app/inventory/Point of Sale/ClosingAmounts.json';
|
||||
import ClosingCash from './app/inventory/Point of Sale/ClosingCash.json';
|
||||
import DefaultCashDenominations from './app/inventory/Point of Sale/DefaultCashDenominations.json';
|
||||
import OpeningAmounts from './app/inventory/Point of Sale/OpeningAmounts.json';
|
||||
import OpeningCash from './app/inventory/Point of Sale/OpeningCash.json';
|
||||
import POSSettings from './app/inventory/Point of Sale/POSSettings.json';
|
||||
import POSShift from './app/inventory/Point of Sale/POSShift.json';
|
||||
import POSShiftAmounts from './app/inventory/Point of Sale/POSShiftAmounts.json';
|
||||
import { Schema, SchemaStub } from './types';
|
||||
|
||||
export const coreSchemas: Schema[] = [
|
||||
@ -99,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,
|
||||
@ -129,4 +142,14 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
||||
|
||||
CustomForm as Schema,
|
||||
CustomField as Schema,
|
||||
|
||||
CashDenominations as Schema,
|
||||
ClosingAmounts as Schema,
|
||||
ClosingCash as Schema,
|
||||
DefaultCashDenominations as Schema,
|
||||
OpeningAmounts as Schema,
|
||||
OpeningCash as Schema,
|
||||
POSSettings as Schema,
|
||||
POSShift as Schema,
|
||||
POSShiftAmounts as Schema,
|
||||
];
|
||||
|
@ -196,7 +196,7 @@ export default {
|
||||
this.padding +
|
||||
this.left +
|
||||
(i * (this.viewBoxWidth - this.left - 2 * this.padding)) /
|
||||
(this.count - 1)
|
||||
(this.count - 1 || 1) // The "or" one (1) prevents accidentally dividing by 0
|
||||
);
|
||||
},
|
||||
z() {
|
||||
|
@ -193,7 +193,7 @@ export default {
|
||||
this.padding +
|
||||
this.left +
|
||||
(i * (this.viewBoxWidth - this.left - 2 * this.padding)) /
|
||||
(this.count - 1)
|
||||
(this.count - 1 || 1) // The "or" one (1) prevents accidentally dividing by 0
|
||||
);
|
||||
},
|
||||
ys() {
|
||||
|
@ -62,6 +62,14 @@ import { defineComponent, PropType } from 'vue';
|
||||
import FeatherIcon from '../FeatherIcon.vue';
|
||||
import Base from './Base.vue';
|
||||
|
||||
const mime_types: Record<string, string> = {
|
||||
png: 'image/png',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
webp: 'image/webp',
|
||||
svg: 'image/svg+xml',
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AttachImage',
|
||||
components: { FeatherIcon },
|
||||
@ -99,9 +107,7 @@ export default defineComponent({
|
||||
}
|
||||
const options = {
|
||||
title: fyo.t`Select Image`,
|
||||
filters: [
|
||||
{ name: 'Image', extensions: ['png', 'jpg', 'jpeg', 'webp'] },
|
||||
],
|
||||
filters: [{ name: 'Image', extensions: Object.keys(mime_types) }],
|
||||
};
|
||||
|
||||
const { name, success, data } = await ipc.selectFile(options);
|
||||
@ -110,7 +116,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
const extension = name.split('.').at(-1);
|
||||
const type = 'image/' + extension;
|
||||
const type = mime_types[extension];
|
||||
const dataURL = await getDataURL(type, data);
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -71,6 +71,7 @@ export default {
|
||||
return this.df.options;
|
||||
},
|
||||
selectedColorLabel() {
|
||||
if (!this.colors) return this.value;
|
||||
const color = this.colors.find((c) => this.value === c.value);
|
||||
return color ? color.label : this.value;
|
||||
},
|
||||
|
@ -9,6 +9,7 @@ import Inventory from './inventory.vue';
|
||||
import Invoice from './invoice.vue';
|
||||
import Item from './item.vue';
|
||||
import Mail from './mail.vue';
|
||||
import POS from './pos.vue';
|
||||
import OpeningAc from './opening-ac.vue';
|
||||
import Percentage from './percentage.vue';
|
||||
import Property from './property.vue';
|
||||
@ -36,6 +37,7 @@ export default {
|
||||
'invoice': Invoice,
|
||||
'item': Item,
|
||||
'mail': Mail,
|
||||
'pos': POS,
|
||||
'opening-ac': OpeningAc,
|
||||
'percentage': Percentage,
|
||||
'property': Property,
|
||||
|
15
src/components/Icons/18/pos.vue
Normal file
15
src/components/Icons/18/pos.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
:fill="darkColor"
|
||||
d="M21 13V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V13H2V11L3 6H21L22 11V13H21ZM5 13V19H19V13H5ZM6 14H14V17H6V14ZM3 3H21V5H3V3Z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
import Base from '../base.vue';
|
||||
export default {
|
||||
extends: Base,
|
||||
};
|
||||
</script>
|
116
src/components/POS/FloatingLabelCurrencyInput.vue
Normal file
116
src/components/POS/FloatingLabelCurrencyInput.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="inputType"
|
||||
:class="[inputClasses, size === 'large' ? 'text-lg' : 'text-sm']"
|
||||
:value="round(value)"
|
||||
:max="isNumeric(df) ? df.maxvalue : undefined"
|
||||
:min="isNumeric(df) ? df.minvalue : undefined"
|
||||
:readonly="isReadOnly"
|
||||
:tabindex="isReadOnly ? '-1' : '0'"
|
||||
@blur="onBlur"
|
||||
class="
|
||||
block
|
||||
px-2.5
|
||||
pb-2.5
|
||||
pt-4
|
||||
w-full
|
||||
font-medium
|
||||
text-gray-900
|
||||
bg-gray-25
|
||||
rounded-lg
|
||||
border border-gray-200
|
||||
appearance-none
|
||||
focus:outline-none focus:ring-0
|
||||
peer
|
||||
"
|
||||
/>
|
||||
<label
|
||||
for="floating_outlined"
|
||||
:class="size === 'large' ? 'text-xl' : 'text-md'"
|
||||
class="
|
||||
absolute
|
||||
font-medium
|
||||
text-gray-500
|
||||
duration-300
|
||||
transform
|
||||
-translate-y-4
|
||||
scale-75
|
||||
top-8
|
||||
z-10
|
||||
origin-[0]
|
||||
bg-white2
|
||||
px-2
|
||||
peer-focus:px-2 peer-focus:text-blue-600
|
||||
peer-placeholder-shown:scale-100
|
||||
peer-placeholder-shown:-translate-y-1/2
|
||||
peer-placeholder-shown:top-1/2
|
||||
peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4
|
||||
left-1
|
||||
"
|
||||
>{{ currency ? fyo.currencySymbols[currency] : undefined }}</label
|
||||
>
|
||||
<label
|
||||
for="floating_outlined"
|
||||
:class="size === 'large' ? 'text-xl' : 'text-md'"
|
||||
class="
|
||||
absolute
|
||||
font-medium
|
||||
text-gray-500
|
||||
duration-300
|
||||
transform
|
||||
-translate-y-4
|
||||
scale-75
|
||||
top-1
|
||||
z-10
|
||||
origin-[0]
|
||||
bg-white2
|
||||
px-2
|
||||
peer-focus:px-2 peer-focus:text-blue-600
|
||||
peer-placeholder-shown:scale-100
|
||||
peer-placeholder-shown:-translate-y-1/2
|
||||
peer-placeholder-shown:top-1/2
|
||||
peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4
|
||||
left-1
|
||||
"
|
||||
>{{ df.label }}</label
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import FloatingLabelInputBase from './FloatingLabelInputBase.vue';
|
||||
import { safeParsePesa } from 'utils/index';
|
||||
import { isPesa } from 'fyo/utils';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { defineComponent } from 'vue';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FloatingLabelCurrencyInput',
|
||||
extends: FloatingLabelInputBase,
|
||||
computed: {
|
||||
currency(): string | undefined {
|
||||
if (this.value) {
|
||||
return (this.value as Money).getCurrency();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
round(v: unknown) {
|
||||
if (!isPesa(v)) {
|
||||
v = this.parse(v);
|
||||
}
|
||||
|
||||
if (isPesa(v)) {
|
||||
return v.round();
|
||||
}
|
||||
|
||||
return fyo.pesa(0).round();
|
||||
},
|
||||
parse(value: unknown): Money {
|
||||
return safeParsePesa(value, this.fyo);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
14
src/components/POS/FloatingLabelFloatInput.vue
Normal file
14
src/components/POS/FloatingLabelFloatInput.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FloatingLabelInputBase from './FloatingLabelInputBase.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FloatingLabelFloatInput',
|
||||
extends: FloatingLabelInputBase,
|
||||
computed: {
|
||||
inputType() {
|
||||
return 'number';
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
63
src/components/POS/FloatingLabelInputBase.vue
Normal file
63
src/components/POS/FloatingLabelInputBase.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="inputType"
|
||||
:class="[inputClasses, size === 'large' ? 'text-lg' : 'text-sm']"
|
||||
:value="value"
|
||||
:max="isNumeric(df) ? df.maxvalue : undefined"
|
||||
:min="isNumeric(df) ? df.minvalue : undefined"
|
||||
:readonly="isReadOnly"
|
||||
:tabindex="isReadOnly ? '-1' : '0'"
|
||||
@blur="onBlur"
|
||||
class="
|
||||
block
|
||||
px-2.5
|
||||
pb-2.5
|
||||
pt-4
|
||||
w-full
|
||||
font-medium
|
||||
text-gray-900
|
||||
bg-gray-25
|
||||
rounded-lg
|
||||
border border-gray-200
|
||||
appearance-none
|
||||
focus:outline-none focus:ring-0
|
||||
peer
|
||||
"
|
||||
/>
|
||||
<label
|
||||
for="floating_outlined"
|
||||
:class="size === 'large' ? 'text-xl' : 'text-md'"
|
||||
class="
|
||||
absolute
|
||||
font-medium
|
||||
text-gray-500
|
||||
duration-300
|
||||
transform
|
||||
-translate-y-4
|
||||
scale-75
|
||||
top-1
|
||||
z-10
|
||||
origin-[0]
|
||||
bg-white2
|
||||
px-2
|
||||
peer-focus:px-2 peer-focus:text-blue-600 peer-focus:dark:text-blue-500
|
||||
peer-placeholder-shown:scale-100
|
||||
peer-placeholder-shown:-translate-y-1/2
|
||||
peer-placeholder-shown:top-1/2
|
||||
peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4
|
||||
left-1
|
||||
"
|
||||
>{{ df.label }}</label
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Base from '../Controls/Base.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FloatingLabelInputBase',
|
||||
extends: Base,
|
||||
});
|
||||
</script>
|
166
src/components/POS/ItemsTable.vue
Normal file
166
src/components/POS/ItemsTable.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<Row
|
||||
:ratio="ratio"
|
||||
class="border flex items-center mt-4 px-2 rounded-t-md text-gray-600 w-full"
|
||||
>
|
||||
<div
|
||||
v-for="df in tableFields"
|
||||
:key="df.fieldname"
|
||||
class="flex items-center px-2 py-2 text-lg"
|
||||
:class="{
|
||||
'ms-auto': isNumeric(df as Field),
|
||||
}"
|
||||
:style="{
|
||||
height: ``,
|
||||
}"
|
||||
>
|
||||
{{ df.label }}
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<div class="overflow-y-auto" style="height: 72.5vh">
|
||||
<Row
|
||||
v-if="items"
|
||||
v-for="row in items"
|
||||
:ratio="ratio"
|
||||
:border="true"
|
||||
class="
|
||||
border-b border-l border-r
|
||||
flex
|
||||
group
|
||||
h-row-mid
|
||||
hover:bg-gray-25
|
||||
items-center
|
||||
justify-center
|
||||
px-2
|
||||
w-full
|
||||
"
|
||||
@click="handleChange(row as POSItem)"
|
||||
>
|
||||
<FormControl
|
||||
v-for="df in tableFields"
|
||||
:key="df.fieldname"
|
||||
size="large"
|
||||
class=""
|
||||
:df="df"
|
||||
:value="row[df.fieldname]"
|
||||
:readOnly="true"
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import FormControl from '../Controls/FormControl.vue';
|
||||
import Row from 'src/components/Row.vue';
|
||||
import { isNumeric } from 'src/utils';
|
||||
import { inject } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { defineComponent } from 'vue';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Field } from 'schemas/types';
|
||||
import { ItemQtyMap } from './types';
|
||||
import { Item } from 'models/baseModels/Item/Item';
|
||||
import { POSItem } from './types';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ItemsTable',
|
||||
components: { FormControl, Row },
|
||||
emits: ['addItem', 'updateValues'],
|
||||
setup() {
|
||||
return {
|
||||
itemQtyMap: inject('itemQtyMap') as ItemQtyMap,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [] as POSItem[],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ratio() {
|
||||
return [1, 1, 1, 0.7];
|
||||
},
|
||||
tableFields() {
|
||||
return [
|
||||
{
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
label: 'Item',
|
||||
placeholder: 'Item',
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
fieldname: 'rate',
|
||||
label: 'Rate',
|
||||
placeholder: 'Rate',
|
||||
fieldtype: 'Currency',
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
fieldname: 'availableQty',
|
||||
label: 'Available Qty',
|
||||
placeholder: 'Available Qty',
|
||||
fieldtype: 'Float',
|
||||
readOnly: true,
|
||||
},
|
||||
{
|
||||
fieldname: 'unit',
|
||||
label: 'Unit',
|
||||
placeholder: 'Unit',
|
||||
fieldtype: 'Data',
|
||||
target: 'UOM',
|
||||
readOnly: true,
|
||||
},
|
||||
] as Field[];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
itemQtyMap: {
|
||||
async handler() {
|
||||
this.setItems();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
await this.setItems();
|
||||
},
|
||||
methods: {
|
||||
async setItems() {
|
||||
const items = (await fyo.db.getAll(ModelNameEnum.Item, {
|
||||
fields: [],
|
||||
filters: { trackItem: true },
|
||||
})) as Item[];
|
||||
|
||||
this.items = [] as POSItem[];
|
||||
for (const item of items) {
|
||||
let availableQty = 0;
|
||||
|
||||
if (!!this.itemQtyMap[item.name as string]) {
|
||||
availableQty = this.itemQtyMap[item.name as string].availableQty;
|
||||
}
|
||||
|
||||
if (!item.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.items.push({
|
||||
availableQty,
|
||||
name: item.name,
|
||||
rate: item.rate as Money,
|
||||
unit: item.unit as string,
|
||||
hasBatch: !!item.hasBatch,
|
||||
hasSerialNumber: !!item.hasSerialNumber,
|
||||
});
|
||||
}
|
||||
},
|
||||
handleChange(value: POSItem) {
|
||||
this.$emit('addItem', value);
|
||||
this.$emit('updateValues');
|
||||
},
|
||||
isNumeric,
|
||||
},
|
||||
});
|
||||
</script>
|
354
src/components/POS/SelectedItemRow.vue
Normal file
354
src/components/POS/SelectedItemRow.vue
Normal file
@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<feather-icon
|
||||
:name="isExapanded ? 'chevron-up' : 'chevron-down'"
|
||||
class="w-4 h-4 inline-flex"
|
||||
@click="isExapanded = !isExapanded"
|
||||
/>
|
||||
|
||||
<Link
|
||||
:df="{
|
||||
fieldname: 'item',
|
||||
fieldtype: 'Data',
|
||||
label: 'item',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.item"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<Int
|
||||
:df="{
|
||||
fieldname: 'quantity',
|
||||
fieldtype: 'Int',
|
||||
label: 'Quantity',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.quantity"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<Link
|
||||
:df="{
|
||||
fieldname: 'unit',
|
||||
fieldtype: 'Data',
|
||||
label: 'Unit',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.unit"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="{
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'rate',
|
||||
label: 'rate',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.rate"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="{
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'amount',
|
||||
label: 'Amount',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.amount"
|
||||
:read-only="true"
|
||||
/>
|
||||
|
||||
<div class="px-4">
|
||||
<feather-icon
|
||||
name="trash"
|
||||
class="w-4 text-xl text-red-500"
|
||||
@click="$emit('removeItem', row.idx)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
|
||||
<template v-if="isExapanded">
|
||||
<div class="px-4 pt-6 col-span-1">
|
||||
<Float
|
||||
:df="{
|
||||
fieldname: 'quantity',
|
||||
fieldtype: 'Float',
|
||||
label: 'Quantity',
|
||||
}"
|
||||
size="medium"
|
||||
:min="0"
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:value="row.quantity"
|
||||
@change="(value:number) => (row.quantity = value)"
|
||||
:read-only="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pt-6 col-span-2 flex">
|
||||
<Link
|
||||
v-if="isUOMConversionEnabled"
|
||||
:df="{
|
||||
fieldname: 'transferUnit',
|
||||
fieldtype: 'Link',
|
||||
target: 'UOM',
|
||||
label: t`Transfer Unit`,
|
||||
}"
|
||||
class="flex-1"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.transferUnit"
|
||||
@change="(value:string) => setTransferUnit((row.transferUnit = value))"
|
||||
/>
|
||||
<feather-icon
|
||||
v-if="isUOMConversionEnabled"
|
||||
name="refresh-ccw"
|
||||
class="w-3.5 ml-2 mt-4 text-blue-500"
|
||||
@click="row.transferUnit = row.unit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pt-6 col-span-2">
|
||||
<Int
|
||||
v-if="isUOMConversionEnabled"
|
||||
:df="{
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'transferQuantity',
|
||||
label: 'Transfer Quantity',
|
||||
}"
|
||||
size="medium"
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:value="row.transferQuantity"
|
||||
@change="(value:number) => setTransferQty((row.transferQuantity = value))"
|
||||
:read-only="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
<div></div>
|
||||
|
||||
<div class="px-4 pt-6 flex">
|
||||
<Currency
|
||||
:df="{
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'rate',
|
||||
label: 'Rate',
|
||||
}"
|
||||
size="medium"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.rate"
|
||||
:read-only="false"
|
||||
@change="(value:Money) => (row.rate = value)"
|
||||
/>
|
||||
<feather-icon
|
||||
name="refresh-ccw"
|
||||
class="w-3.5 ml-2 mt-5 text-blue-500 flex-none"
|
||||
@click="row.rate= (defaultRate as Money)"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-6 pt-6 col-span-2">
|
||||
<Currency
|
||||
v-if="isDiscountingEnabled"
|
||||
:df="{
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'discountAmount',
|
||||
label: 'Discount Amount',
|
||||
}"
|
||||
class="col-span-2"
|
||||
size="medium"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.itemDiscountAmount"
|
||||
:read-only="row.itemDiscountPercent as number > 0"
|
||||
@change="(value:number) => setItemDiscount('amount', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pt-6 col-span-2">
|
||||
<Float
|
||||
v-if="isDiscountingEnabled"
|
||||
:df="{
|
||||
fieldtype: 'Float',
|
||||
fieldname: 'itemDiscountPercent',
|
||||
label: 'Discount Percent',
|
||||
}"
|
||||
size="medium"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.itemDiscountPercent"
|
||||
:read-only="!row.itemDiscountAmount?.isZero()"
|
||||
@change="(value:number) => setItemDiscount('percent', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class=""></div>
|
||||
|
||||
<div
|
||||
v-if="row.links?.item && row.links?.item.hasBatch"
|
||||
class="pl-6 px-4 pt-6 col-span-2"
|
||||
>
|
||||
<Link
|
||||
:df="{
|
||||
fieldname: 'batch',
|
||||
fieldtype: 'Link',
|
||||
target: 'Batch',
|
||||
label: t`Batch`,
|
||||
}"
|
||||
value=""
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:read-only="false"
|
||||
@change="(value:string) => setBatch(value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="row.links?.item && row.links?.item.hasBatch"
|
||||
class="px-2 pt-6 col-span-2"
|
||||
>
|
||||
<Float
|
||||
:df="{
|
||||
fieldname: 'availableQtyInBatch',
|
||||
fieldtype: 'Float',
|
||||
label: t`Qty in Batch`,
|
||||
}"
|
||||
size="medium"
|
||||
:min="0"
|
||||
:value="availableQtyInBatch"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="hasSerialNumber" class="px-2 pt-8 col-span-2">
|
||||
<Text
|
||||
:df="{
|
||||
label: t`Serial Number`,
|
||||
fieldtype: 'Text',
|
||||
fieldname: 'serialNumber',
|
||||
}"
|
||||
:value="row.serialNumber"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:required="hasSerialNumber"
|
||||
@change="(value:string)=> setSerialNumber(value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Currency from '../Controls/Currency.vue';
|
||||
import Data from '../Controls/Data.vue';
|
||||
import Float from '../Controls/Float.vue';
|
||||
import Int from '../Controls/Int.vue';
|
||||
import Link from '../Controls/Link.vue';
|
||||
import Text from '../Controls/Text.vue';
|
||||
import { inject } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { defineComponent } from 'vue';
|
||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { Money } from 'pesa';
|
||||
import { DiscountType } from './types';
|
||||
import { t } from 'fyo';
|
||||
import { validateSerialNumberCount } from 'src/utils/pos';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SelectedItemRow',
|
||||
components: { Currency, Data, Float, Int, Link, Text },
|
||||
props: {
|
||||
row: { type: SalesInvoiceItem, required: true },
|
||||
},
|
||||
emits: ['removeItem', 'runSinvFormulas', 'setItemSerialNumbers'],
|
||||
setup() {
|
||||
return {
|
||||
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
|
||||
itemSerialNumbers: inject('itemSerialNumbers') as {
|
||||
[item: string]: string;
|
||||
},
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExapanded: false,
|
||||
batches: [] as string[],
|
||||
availableQtyInBatch: 0,
|
||||
|
||||
defaultRate: this.row.rate as Money,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isUOMConversionEnabled(): boolean {
|
||||
return !!fyo.singles.InventorySettings?.enableUomConversions;
|
||||
},
|
||||
hasSerialNumber(): boolean {
|
||||
return !!(this.row.links?.item && this.row.links?.item.hasSerialNumber);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getAvailableQtyInBatch(): Promise<number> {
|
||||
if (!this.row.batch) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (
|
||||
(await fyo.db.getStockQuantity(
|
||||
this.row.item as string,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
this.row.batch
|
||||
)) ?? 0
|
||||
);
|
||||
},
|
||||
async setBatch(batch: string) {
|
||||
this.row.batch = batch;
|
||||
this.availableQtyInBatch = await this.getAvailableQtyInBatch();
|
||||
},
|
||||
setSerialNumber(serialNumber: string) {
|
||||
if (!serialNumber) {
|
||||
return;
|
||||
}
|
||||
this.itemSerialNumbers[this.row.item as string] = serialNumber;
|
||||
|
||||
validateSerialNumberCount(
|
||||
serialNumber,
|
||||
this.row.quantity ?? 0,
|
||||
this.row.item!
|
||||
);
|
||||
},
|
||||
setItemDiscount(type: DiscountType, value: Money | number) {
|
||||
if (type === 'percent') {
|
||||
this.row.setItemDiscountAmount = false;
|
||||
this.row.itemDiscountPercent = value as number;
|
||||
this.$emit('runSinvFormulas');
|
||||
return;
|
||||
}
|
||||
this.row.setItemDiscountAmount = true;
|
||||
this.row.itemDiscountAmount = value as Money;
|
||||
this.$emit('runSinvFormulas');
|
||||
},
|
||||
setTransferUnit(unit: string) {
|
||||
this.row.setTransferUnit = unit;
|
||||
this.row._applyFormula('transferUnit');
|
||||
},
|
||||
setTransferQty(quantity: number) {
|
||||
this.row.transferQuantity = quantity;
|
||||
this.row._applyFormula('transferQuantity');
|
||||
this.$emit('runSinvFormulas');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
150
src/components/POS/SelectedItemTable.vue
Normal file
150
src/components/POS/SelectedItemTable.vue
Normal file
@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<Row
|
||||
:ratio="ratio"
|
||||
class="border rounded-t px-2 text-gray-600 w-full flex items-center mt-4"
|
||||
>
|
||||
<div
|
||||
v-if="tableFields"
|
||||
v-for="df in tableFields"
|
||||
:key="df.fieldname"
|
||||
class="items-center text-lg flex px-2 py-2"
|
||||
:class="{
|
||||
'ms-auto': isNumeric(df as Field),
|
||||
}"
|
||||
:style="{
|
||||
height: ``,
|
||||
}"
|
||||
>
|
||||
{{ df.label }}
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
<div class="overflow-y-auto" style="height: 50vh">
|
||||
<Row
|
||||
v-for="row in sinvDoc.items"
|
||||
:ratio="ratio"
|
||||
class="
|
||||
border
|
||||
w-full
|
||||
px-2
|
||||
py-2
|
||||
group
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
hover:bg-gray-25
|
||||
"
|
||||
>
|
||||
<SelectedItemRow
|
||||
:row="(row as SalesInvoiceItem)"
|
||||
@remove-item="removeItem"
|
||||
@run-sinv-formulas="runSinvFormulas"
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import FormContainer from '../FormContainer.vue';
|
||||
import FormControl from '../Controls/FormControl.vue';
|
||||
import Link from '../Controls/Link.vue';
|
||||
import Row from '../Row.vue';
|
||||
import RowEditForm from 'src/pages/CommonForm/RowEditForm.vue';
|
||||
import SelectedItemRow from './SelectedItemRow.vue';
|
||||
import { isNumeric } from 'src/utils';
|
||||
import { inject } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { Field } from 'schemas/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SelectedItemTable',
|
||||
components: {
|
||||
FormContainer,
|
||||
FormControl,
|
||||
Link,
|
||||
Row,
|
||||
RowEditForm,
|
||||
SelectedItemRow,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
sinvDoc: inject('sinvDoc') as SalesInvoice,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExapanded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ratio() {
|
||||
return [0.1, 1, 0.8, 0.8, 0.8, 0.8, 0.2];
|
||||
},
|
||||
tableFields() {
|
||||
return [
|
||||
{
|
||||
fieldname: 'toggler',
|
||||
fieldtype: 'Link',
|
||||
label: ' ',
|
||||
},
|
||||
{
|
||||
fieldname: 'item',
|
||||
fieldtype: 'Link',
|
||||
label: 'Item',
|
||||
placeholder: 'Item',
|
||||
required: true,
|
||||
schemaName: 'Item',
|
||||
},
|
||||
{
|
||||
fieldname: 'quantity',
|
||||
label: 'Quantity',
|
||||
placeholder: 'Quantity',
|
||||
fieldtype: 'Int',
|
||||
required: true,
|
||||
schemaName: '',
|
||||
},
|
||||
{
|
||||
fieldname: 'unit',
|
||||
label: 'Stock Unit',
|
||||
placeholder: 'Unit',
|
||||
fieldtype: 'Link',
|
||||
required: true,
|
||||
schemaName: 'UOM',
|
||||
},
|
||||
{
|
||||
fieldname: 'rate',
|
||||
label: 'Rate',
|
||||
placeholder: 'Rate',
|
||||
fieldtype: 'Currency',
|
||||
required: true,
|
||||
schemaName: '',
|
||||
},
|
||||
{
|
||||
fieldname: 'amount',
|
||||
label: 'Amount',
|
||||
placeholder: 'Amount',
|
||||
fieldtype: 'Currency',
|
||||
required: true,
|
||||
schemaName: '',
|
||||
},
|
||||
{
|
||||
fieldname: 'removeItem',
|
||||
fieldtype: 'Link',
|
||||
label: ' ',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeItem(idx: number) {
|
||||
this.sinvDoc.remove('items', idx);
|
||||
},
|
||||
async runSinvFormulas() {
|
||||
await this.sinvDoc.runFormulas();
|
||||
},
|
||||
isNumeric,
|
||||
},
|
||||
});
|
||||
</script>
|
20
src/components/POS/types.ts
Normal file
20
src/components/POS/types.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Money } from "pesa";
|
||||
|
||||
export type ItemQtyMap = {
|
||||
[item: string]: { availableQty: number;[batch: string]: number };
|
||||
}
|
||||
|
||||
export type ItemSerialNumbers = { [item: string]: string };
|
||||
|
||||
export type DiscountType = "percent" | "amount";
|
||||
|
||||
export type ModalName = 'ShiftOpen' | 'ShiftClose' | 'Payment'
|
||||
|
||||
export interface POSItem {
|
||||
name: string,
|
||||
rate: Money,
|
||||
availableQty: number,
|
||||
unit: string,
|
||||
hasBatch: boolean,
|
||||
hasSerialNumber: boolean,
|
||||
}
|
@ -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>
|
||||
|
209
src/pages/POS/ClosePOSShiftModal.vue
Normal file
209
src/pages/POS/ClosePOSShiftModal.vue
Normal file
@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<Modal :open-modal="openModal" class="w-3/6 p-4">
|
||||
<h1 class="text-xl font-semibold text-center pb-4">Close POS Shift</h1>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-medium">Closing Cash</h2>
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="text-base"
|
||||
:df="getField('closingCash')"
|
||||
:show-header="true"
|
||||
:border="true"
|
||||
:value="posShiftDoc?.closingCash ?? []"
|
||||
:read-only="false"
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<h2 class="mt-6 mb-2 text-lg font-medium">Closing Amounts</h2>
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="text-base"
|
||||
:df="getField('closingAmounts')"
|
||||
:show-header="true"
|
||||
:border="true"
|
||||
:value="posShiftDoc?.closingAmounts"
|
||||
:read-only="true"
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-4 flex items-end">
|
||||
<Button
|
||||
class="w-full py-5 bg-red-500"
|
||||
@click="$emit('toggleModal', 'ShiftClose', false)"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Button class="w-full py-5 bg-green-500" @click="handleSubmit">
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import Table from 'src/components/Controls/Table.vue';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { OpeningAmounts } from 'models/inventory/Point of Sale/OpeningAmounts';
|
||||
import { POSShift } from 'models/inventory/Point of Sale/POSShift';
|
||||
import { computed } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { t } from 'fyo';
|
||||
import {
|
||||
validateClosingAmounts,
|
||||
transferPOSCashAndWriteOff,
|
||||
} from 'src/utils/pos';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ClosePOSShiftModal',
|
||||
components: { Button, Modal, Table },
|
||||
provide() {
|
||||
return {
|
||||
doc: computed(() => this.posShiftDoc),
|
||||
};
|
||||
},
|
||||
props: {
|
||||
openModal: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
emits: ['toggleModal'],
|
||||
data() {
|
||||
return {
|
||||
isValuesSeeded: false,
|
||||
|
||||
posShiftDoc: undefined as POSShift | undefined,
|
||||
transactedAmount: {} as Record<string, Money> | undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
openModal: {
|
||||
async handler() {
|
||||
await this.setTransactedAmount();
|
||||
await this.seedClosingAmounts();
|
||||
},
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
this.posShiftDoc = fyo.singles[ModelNameEnum.POSShift];
|
||||
await this.seedValues();
|
||||
await this.setTransactedAmount();
|
||||
},
|
||||
methods: {
|
||||
async setTransactedAmount() {
|
||||
if (!fyo.singles.POSShift?.openingDate) {
|
||||
return;
|
||||
}
|
||||
const fromDate = fyo.singles.POSShift?.openingDate;
|
||||
this.transactedAmount = await fyo.db.getPOSTransactedAmount(
|
||||
fromDate,
|
||||
new Date(),
|
||||
fyo.singles.POSShift.closingDate as Date
|
||||
);
|
||||
},
|
||||
seedClosingCash() {
|
||||
if (!this.posShiftDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.closingCash = [];
|
||||
|
||||
this.posShiftDoc?.openingCash?.map(async (row) => {
|
||||
await this.posShiftDoc?.append('closingCash', {
|
||||
count: row.count,
|
||||
denomination: row.denomination as Money,
|
||||
});
|
||||
});
|
||||
},
|
||||
async seedClosingAmounts() {
|
||||
if (!this.posShiftDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.closingAmounts = [];
|
||||
await this.posShiftDoc.sync();
|
||||
|
||||
const openingAmounts = this.posShiftDoc
|
||||
.openingAmounts as OpeningAmounts[];
|
||||
|
||||
for (const row of openingAmounts) {
|
||||
if (!row.paymentMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
let expectedAmount = fyo.pesa(0);
|
||||
|
||||
if (row.paymentMethod === 'Cash') {
|
||||
expectedAmount = expectedAmount.add(
|
||||
this.posShiftDoc.openingCashAmount as Money
|
||||
);
|
||||
}
|
||||
|
||||
if (row.paymentMethod === 'Transfer') {
|
||||
expectedAmount = expectedAmount.add(
|
||||
this.posShiftDoc.openingTransferAmount as Money
|
||||
);
|
||||
}
|
||||
|
||||
if (this.transactedAmount) {
|
||||
expectedAmount = expectedAmount.add(
|
||||
this.transactedAmount[row.paymentMethod]
|
||||
);
|
||||
}
|
||||
|
||||
await this.posShiftDoc.append('closingAmounts', {
|
||||
paymentMethod: row.paymentMethod,
|
||||
openingAmount: row.amount,
|
||||
closingAmount: fyo.pesa(0),
|
||||
expectedAmount: expectedAmount,
|
||||
differenceAmount: fyo.pesa(0),
|
||||
});
|
||||
await this.posShiftDoc.sync();
|
||||
}
|
||||
},
|
||||
async seedValues() {
|
||||
this.isValuesSeeded = false;
|
||||
this.seedClosingCash();
|
||||
await this.seedClosingAmounts();
|
||||
this.isValuesSeeded = true;
|
||||
},
|
||||
getField(fieldname: string) {
|
||||
return fyo.getField(ModelNameEnum.POSShift, fieldname);
|
||||
},
|
||||
async handleChange() {
|
||||
await this.posShiftDoc?.sync();
|
||||
},
|
||||
async handleSubmit() {
|
||||
try {
|
||||
validateClosingAmounts(this.posShiftDoc as POSShift);
|
||||
await this.posShiftDoc?.set('isShiftOpen', false);
|
||||
await this.posShiftDoc?.set('closingDate', new Date());
|
||||
await this.posShiftDoc?.sync();
|
||||
await transferPOSCashAndWriteOff(fyo, this.posShiftDoc as POSShift);
|
||||
|
||||
this.$emit('toggleModal', 'ShiftClose');
|
||||
} catch (error) {
|
||||
return showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
duration: 'short',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
223
src/pages/POS/OpenPOSShiftModal.vue
Normal file
223
src/pages/POS/OpenPOSShiftModal.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<Modal class="w-3/6 p-4">
|
||||
<h1 class="text-xl font-semibold text-center pb-4">Open POS Shift</h1>
|
||||
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<div class="col-span-6">
|
||||
<h2 class="text-lg font-medium">Cash In Denominations</h2>
|
||||
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="mt-4 text-base"
|
||||
:df="getField('openingCash')"
|
||||
:show-header="true"
|
||||
:border="true"
|
||||
:value="posShiftDoc?.openingCash"
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-6">
|
||||
<h2 class="text-lg font-medium">Opening Amount</h2>
|
||||
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="mt-4 text-base"
|
||||
:df="getField('openingAmounts')"
|
||||
:show-header="true"
|
||||
:border="true"
|
||||
:max-rows-before-overflow="4"
|
||||
:value="posShiftDoc?.openingAmounts"
|
||||
:read-only="true"
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-4 flex items-end">
|
||||
<Button class="w-full py-5 bg-red-500" @click="$router.back()">
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Back` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Button class="w-full py-5 bg-green-500" @click="handleSubmit">
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import Table from 'src/components/Controls/Table.vue';
|
||||
import { AccountTypeEnum } from 'models/baseModels/Account/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { POSShift } from 'models/inventory/Point of Sale/POSShift';
|
||||
import { computed } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import { t } from 'fyo';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OpenPOSShift',
|
||||
components: { Button, Modal, Table },
|
||||
provide() {
|
||||
return {
|
||||
doc: computed(() => this.posShiftDoc),
|
||||
};
|
||||
},
|
||||
emits: ['toggleModal'],
|
||||
data() {
|
||||
return {
|
||||
posShiftDoc: undefined as POSShift | undefined,
|
||||
|
||||
isValuesSeeded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
getDefaultCashDenominations() {
|
||||
return this.fyo.singles.Defaults?.posCashDenominations;
|
||||
},
|
||||
posCashAccount() {
|
||||
return fyo.singles.POSSettings?.cashAccount;
|
||||
},
|
||||
posOpeningCashAmount(): Money {
|
||||
return this.posShiftDoc?.openingCashAmount as Money;
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.isValuesSeeded = false;
|
||||
this.posShiftDoc = fyo.singles[ModelNameEnum.POSShift];
|
||||
|
||||
await this.seedDefaults();
|
||||
this.isValuesSeeded = true;
|
||||
},
|
||||
methods: {
|
||||
async seedDefaultCashDenomiations() {
|
||||
if (!this.posShiftDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.openingCash = [];
|
||||
await this.posShiftDoc.sync();
|
||||
|
||||
const denominations = this.getDefaultCashDenominations;
|
||||
|
||||
if (!denominations) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of denominations) {
|
||||
await this.posShiftDoc.append('openingCash', {
|
||||
denomination: row.denomination,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
await this.posShiftDoc.sync();
|
||||
}
|
||||
},
|
||||
async seedPaymentMethods() {
|
||||
if (!this.posShiftDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.openingAmounts = [];
|
||||
await this.posShiftDoc.sync();
|
||||
|
||||
await this.posShiftDoc.set('openingAmounts', [
|
||||
{
|
||||
paymentMethod: 'Cash',
|
||||
amount: fyo.pesa(0),
|
||||
},
|
||||
{
|
||||
paymentMethod: 'Transfer',
|
||||
amount: fyo.pesa(0),
|
||||
},
|
||||
]);
|
||||
await this.posShiftDoc.sync();
|
||||
},
|
||||
async seedDefaults() {
|
||||
if (!!this.posShiftDoc?.isShiftOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.seedDefaultCashDenomiations();
|
||||
await this.seedPaymentMethods();
|
||||
},
|
||||
getField(fieldname: string) {
|
||||
return this.fyo.getField(ModelNameEnum.POSShift, fieldname);
|
||||
},
|
||||
setOpeningCashAmount() {
|
||||
if (!this.posShiftDoc?.openingAmounts) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.posShiftDoc.openingAmounts.map((row) => {
|
||||
if (row.paymentMethod === 'Cash') {
|
||||
row.amount = this.posShiftDoc?.openingCashAmount as Money;
|
||||
}
|
||||
});
|
||||
},
|
||||
async handleChange() {
|
||||
await this.posShiftDoc?.sync();
|
||||
this.setOpeningCashAmount();
|
||||
},
|
||||
async handleSubmit() {
|
||||
try {
|
||||
if (this.posShiftDoc?.openingCashAmount.isNegative()) {
|
||||
throw new ValidationError(
|
||||
t`Opening Cash Amount can not be negative.`
|
||||
);
|
||||
}
|
||||
|
||||
await this.posShiftDoc?.setMultiple({
|
||||
isShiftOpen: true,
|
||||
openingDate: new Date(),
|
||||
});
|
||||
|
||||
await this.posShiftDoc?.sync();
|
||||
|
||||
if (!this.posShiftDoc?.openingCashAmount.isZero()) {
|
||||
const jvDoc = fyo.doc.getNewDoc(ModelNameEnum.JournalEntry, {
|
||||
entryType: 'Journal Entry',
|
||||
});
|
||||
|
||||
await jvDoc.append('accounts', {
|
||||
account: this.posCashAccount,
|
||||
debit: this.posShiftDoc?.openingCashAmount as Money,
|
||||
credit: this.fyo.pesa(0),
|
||||
});
|
||||
|
||||
await jvDoc.append('accounts', {
|
||||
account: AccountTypeEnum.Cash,
|
||||
debit: this.fyo.pesa(0),
|
||||
credit: this.posShiftDoc?.openingCashAmount as Money,
|
||||
});
|
||||
|
||||
await (await jvDoc.sync()).submit();
|
||||
}
|
||||
|
||||
this.$emit('toggleModal', 'ShiftOpen');
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
duration: 'short',
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
569
src/pages/POS/POS.vue
Normal file
569
src/pages/POS/POS.vue
Normal file
@ -0,0 +1,569 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<PageHeader :title="t`Point of Sale`">
|
||||
<slot>
|
||||
<Button class="bg-red-500" @click="toggleModal('ShiftClose')">
|
||||
<span class="font-medium text-white">{{ t`Close POS Shift ` }}</span>
|
||||
</Button>
|
||||
</slot>
|
||||
</PageHeader>
|
||||
|
||||
<OpenPOSShiftModal
|
||||
v-if="!isPosShiftOpen"
|
||||
:open-modal="!isPosShiftOpen"
|
||||
@toggle-modal="toggleModal"
|
||||
/>
|
||||
|
||||
<ClosePOSShiftModal
|
||||
:open-modal="openShiftCloseModal"
|
||||
@toggle-modal="toggleModal"
|
||||
/>
|
||||
|
||||
<PaymentModal
|
||||
:open-modal="openPaymentModal"
|
||||
@create-transaction="createTransaction"
|
||||
@toggle-modal="toggleModal"
|
||||
@set-cash-amount="setCashAmount"
|
||||
@set-transfer-amount="setTransferAmount"
|
||||
@set-transfer-ref-no="setTransferRefNo"
|
||||
@set-transfer-clearance-date="setTransferClearanceDate"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="bg-gray-25 gap-2 grid grid-cols-12 p-4"
|
||||
style="height: calc(100vh - var(--h-row-largest))"
|
||||
>
|
||||
<div class="bg-white border col-span-5 rounded-md">
|
||||
<div class="rounded-md p-4 col-span-5">
|
||||
<div class="flex gap-x-2">
|
||||
<!-- Item Search -->
|
||||
<Link
|
||||
:class="
|
||||
fyo.singles.InventorySettings?.enableBarcodes
|
||||
? 'flex-shrink-0 w-2/3'
|
||||
: 'w-full'
|
||||
"
|
||||
:df="{
|
||||
label: t`Search an Item`,
|
||||
fieldtype: 'Link',
|
||||
fieldname: 'item',
|
||||
target: 'Item',
|
||||
}"
|
||||
:border="true"
|
||||
:value="itemSearchTerm"
|
||||
@keyup.enter="
|
||||
async () => await addItem(await getItem(itemSearchTerm))
|
||||
"
|
||||
@change="(item: string) =>itemSearchTerm= item"
|
||||
/>
|
||||
|
||||
<Barcode
|
||||
v-if="fyo.singles.InventorySettings?.enableBarcodes"
|
||||
class="w-1/3"
|
||||
@item-selected="
|
||||
async (name: string) => {
|
||||
await addItem(await getItem(name));
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ItemsTable @add-item="addItem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-7">
|
||||
<div class="flex flex-col gap-3" style="height: calc(100vh - 6rem)">
|
||||
<div class="bg-white border grow h-full p-4 rounded-md">
|
||||
<!-- Customer Search -->
|
||||
<Link
|
||||
v-if="sinvDoc.fieldMap"
|
||||
class="flex-shrink-0"
|
||||
:border="true"
|
||||
:value="sinvDoc.party"
|
||||
:df="sinvDoc.fieldMap.party"
|
||||
@change="(value:string) => (sinvDoc.party = value)"
|
||||
/>
|
||||
|
||||
<SelectedItemTable />
|
||||
</div>
|
||||
|
||||
<div class="bg-white border p-4 rounded-md">
|
||||
<div class="w-full grid grid-cols-2 gap-y-2 gap-x-3">
|
||||
<div class="">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<FloatingLabelFloatInput
|
||||
:df="{
|
||||
label: t`Total Quantity`,
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'totalQuantity',
|
||||
minvalue: 0,
|
||||
maxvalue: 1000,
|
||||
}"
|
||||
size="large"
|
||||
:value="totalQuantity"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
/>
|
||||
|
||||
<FloatingLabelCurrencyInput
|
||||
:df="{
|
||||
label: t`Add'l Discounts`,
|
||||
fieldtype: 'Int',
|
||||
fieldname: 'additionalDiscount',
|
||||
minvalue: 0,
|
||||
}"
|
||||
size="large"
|
||||
:value="additionalDiscounts"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
@change="(amount:Money)=> additionalDiscounts= amount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
<FloatingLabelCurrencyInput
|
||||
:df="{
|
||||
label: t`Item Discounts`,
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'itemDiscounts',
|
||||
}"
|
||||
size="large"
|
||||
:value="itemDiscounts"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
/>
|
||||
<FloatingLabelCurrencyInput
|
||||
v-if="sinvDoc.fieldMap"
|
||||
:df="sinvDoc.fieldMap.grandTotal"
|
||||
size="large"
|
||||
:value="sinvDoc.grandTotal"
|
||||
:read-only="true"
|
||||
:text-right="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<Button
|
||||
class="w-full bg-red-500 py-6"
|
||||
:disabled="!sinvDoc.items?.length"
|
||||
@click="clearValues"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
class="mt-4 w-full bg-green-500 py-6"
|
||||
:disabled="disablePayButton"
|
||||
@click="toggleModal('Payment', true)"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Pay` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import ClosePOSShiftModal from './ClosePOSShiftModal.vue';
|
||||
import FloatingLabelCurrencyInput from 'src/components/POS/FloatingLabelCurrencyInput.vue';
|
||||
import FloatingLabelFloatInput from 'src/components/POS/FloatingLabelFloatInput.vue';
|
||||
import ItemsTable from 'src/components/POS/ItemsTable.vue';
|
||||
import Link from 'src/components/Controls/Link.vue';
|
||||
import OpenPOSShiftModal from './OpenPOSShiftModal.vue';
|
||||
import PageHeader from 'src/components/PageHeader.vue';
|
||||
import PaymentModal from './PaymentModal.vue';
|
||||
import SelectedItemTable from 'src/components/POS/SelectedItemTable.vue';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { routeTo, toggleSidebar } from 'src/utils/ui';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { t } from 'fyo';
|
||||
import {
|
||||
ItemQtyMap,
|
||||
ItemSerialNumbers,
|
||||
POSItem,
|
||||
} from 'src/components/POS/types';
|
||||
import { Item } from 'models/baseModels/Item/Item';
|
||||
import { ModalName } from 'src/components/POS/types';
|
||||
import { Money } from 'pesa';
|
||||
import { Payment } from 'models/baseModels/Payment/Payment';
|
||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { Shipment } from 'models/inventory/Shipment';
|
||||
import { showToast } from 'src/utils/interactive';
|
||||
import {
|
||||
getItem,
|
||||
getItemDiscounts,
|
||||
getItemQtyMap,
|
||||
getTotalQuantity,
|
||||
getTotalTaxedAmount,
|
||||
validateIsPosSettingsSet,
|
||||
validateShipment,
|
||||
validateSinv,
|
||||
} from 'src/utils/pos';
|
||||
import Barcode from 'src/components/Controls/Barcode.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'POS',
|
||||
components: {
|
||||
Button,
|
||||
ClosePOSShiftModal,
|
||||
FloatingLabelCurrencyInput,
|
||||
FloatingLabelFloatInput,
|
||||
ItemsTable,
|
||||
Link,
|
||||
OpenPOSShiftModal,
|
||||
PageHeader,
|
||||
PaymentModal,
|
||||
SelectedItemTable,
|
||||
Barcode,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
cashAmount: computed(() => this.cashAmount),
|
||||
doc: computed(() => this.sinvDoc),
|
||||
isDiscountingEnabled: computed(() => this.isDiscountingEnabled),
|
||||
itemDiscounts: computed(() => this.itemDiscounts),
|
||||
itemQtyMap: computed(() => this.itemQtyMap),
|
||||
itemSerialNumbers: computed(() => this.itemSerialNumbers),
|
||||
sinvDoc: computed(() => this.sinvDoc),
|
||||
totalTaxedAmount: computed(() => this.totalTaxedAmount),
|
||||
transferAmount: computed(() => this.transferAmount),
|
||||
transferClearanceDate: computed(() => this.transferClearanceDate),
|
||||
transferRefNo: computed(() => this.transferRefNo),
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isItemsSeeded: false,
|
||||
openPaymentModal: false,
|
||||
openShiftCloseModal: false,
|
||||
openShiftOpenModal: false,
|
||||
|
||||
additionalDiscounts: fyo.pesa(0),
|
||||
cashAmount: fyo.pesa(0),
|
||||
itemDiscounts: fyo.pesa(0),
|
||||
totalTaxedAmount: fyo.pesa(0),
|
||||
transferAmount: fyo.pesa(0),
|
||||
|
||||
totalQuantity: 0,
|
||||
|
||||
defaultCustomer: undefined as string | undefined,
|
||||
itemSearchTerm: '',
|
||||
transferRefNo: undefined as string | undefined,
|
||||
|
||||
transferClearanceDate: undefined as Date | undefined,
|
||||
|
||||
itemQtyMap: {} as ItemQtyMap,
|
||||
itemSerialNumbers: {} as ItemSerialNumbers,
|
||||
paymentDoc: {} as Payment,
|
||||
sinvDoc: {} as SalesInvoice,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
defaultPOSCashAccount: () =>
|
||||
fyo.singles.POSSettings?.cashAccount ?? undefined,
|
||||
isDiscountingEnabled(): boolean {
|
||||
return !!fyo.singles.AccountingSettings?.enableDiscounting;
|
||||
},
|
||||
isPosShiftOpen: () => !!fyo.singles.POSShift?.isShiftOpen,
|
||||
isPaymentAmountSet(): boolean {
|
||||
if (this.sinvDoc.grandTotal?.isZero()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.cashAmount.isZero() && this.transferAmount.isZero()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
disablePayButton(): boolean {
|
||||
if (!this.sinvDoc.items?.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.sinvDoc.party) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sinvDoc: {
|
||||
handler() {
|
||||
this.updateValues();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
toggleSidebar(false);
|
||||
validateIsPosSettingsSet(fyo);
|
||||
this.setSinvDoc();
|
||||
this.setDefaultCustomer();
|
||||
await this.setItemQtyMap();
|
||||
},
|
||||
deactivated() {
|
||||
toggleSidebar(true);
|
||||
},
|
||||
methods: {
|
||||
setCashAmount(amount: Money) {
|
||||
this.cashAmount = amount;
|
||||
},
|
||||
setDefaultCustomer() {
|
||||
this.defaultCustomer = this.fyo.singles.Defaults?.posCustomer ?? '';
|
||||
this.sinvDoc.party = this.defaultCustomer;
|
||||
},
|
||||
setItemDiscounts() {
|
||||
this.itemDiscounts = getItemDiscounts(
|
||||
this.sinvDoc.items as SalesInvoiceItem[]
|
||||
);
|
||||
},
|
||||
async setItemQtyMap() {
|
||||
this.itemQtyMap = await getItemQtyMap();
|
||||
},
|
||||
setSinvDoc() {
|
||||
this.sinvDoc = this.fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
account: 'Debtors',
|
||||
party: this.sinvDoc.party ?? this.defaultCustomer,
|
||||
isPOS: true,
|
||||
}) as SalesInvoice;
|
||||
},
|
||||
setTotalQuantity() {
|
||||
this.totalQuantity = getTotalQuantity(
|
||||
this.sinvDoc.items as SalesInvoiceItem[]
|
||||
);
|
||||
},
|
||||
setTotalTaxedAmount() {
|
||||
this.totalTaxedAmount = getTotalTaxedAmount(this.sinvDoc as SalesInvoice);
|
||||
},
|
||||
setTransferAmount(amount: Money = fyo.pesa(0)) {
|
||||
this.transferAmount = amount;
|
||||
},
|
||||
setTransferClearanceDate(date: Date) {
|
||||
this.transferClearanceDate = date;
|
||||
},
|
||||
setTransferRefNo(ref: string) {
|
||||
this.transferRefNo = ref;
|
||||
},
|
||||
|
||||
async addItem(item: POSItem | Item | undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.sinvDoc.runFormulas();
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.itemQtyMap[item.name as string] ||
|
||||
this.itemQtyMap[item.name as string].availableQty === 0
|
||||
) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`Item ${item.name as string} has Zero Quantity`,
|
||||
duration: 'short',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const existingItems =
|
||||
this.sinvDoc.items?.filter(
|
||||
(invoiceItem) => invoiceItem.item === item.name
|
||||
) ?? [];
|
||||
|
||||
if (item.hasBatch) {
|
||||
for (const item of existingItems) {
|
||||
const itemQty = item.quantity ?? 0;
|
||||
const qtyInBatch =
|
||||
this.itemQtyMap[item.item as string][item.batch as string] ?? 0;
|
||||
|
||||
if (itemQty < qtyInBatch) {
|
||||
item.quantity = (item.quantity as number) + 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.sinvDoc.append('items', {
|
||||
rate: item.rate as Money,
|
||||
item: item.name,
|
||||
});
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingItems.length) {
|
||||
existingItems[0].quantity = (existingItems[0].quantity as number) + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sinvDoc.append('items', {
|
||||
rate: item.rate as Money,
|
||||
item: item.name,
|
||||
});
|
||||
},
|
||||
async createTransaction(shouldPrint = false) {
|
||||
try {
|
||||
await this.validate();
|
||||
await this.submitSinvDoc(shouldPrint);
|
||||
await this.makePayment();
|
||||
await this.makeStockTransfer();
|
||||
await this.afterTransaction();
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async makePayment() {
|
||||
this.paymentDoc = this.sinvDoc.getPayment() as Payment;
|
||||
const paymentMethod = this.cashAmount.isZero() ? 'Transfer' : 'Cash';
|
||||
await this.paymentDoc.set('paymentMethod', paymentMethod);
|
||||
|
||||
if (paymentMethod === 'Transfer') {
|
||||
await this.paymentDoc.setMultiple({
|
||||
amount: this.transferAmount as Money,
|
||||
referenceId: this.transferRefNo,
|
||||
clearanceDate: this.transferClearanceDate,
|
||||
});
|
||||
}
|
||||
|
||||
if (paymentMethod === 'Cash') {
|
||||
await this.paymentDoc.setMultiple({
|
||||
paymentAccount: this.defaultPOSCashAccount,
|
||||
amount: this.cashAmount as Money,
|
||||
});
|
||||
}
|
||||
|
||||
this.paymentDoc.once('afterSubmit', () => {
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: t`Payment ${this.paymentDoc.name as string} is Saved`,
|
||||
duration: 'short',
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await this.paymentDoc?.sync();
|
||||
await this.paymentDoc?.submit();
|
||||
} catch (error) {
|
||||
return showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async makeStockTransfer() {
|
||||
const shipmentDoc = (await this.sinvDoc.getStockTransfer()) as Shipment;
|
||||
if (!shipmentDoc.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of shipmentDoc.items) {
|
||||
item.location = fyo.singles.POSSettings?.inventory;
|
||||
item.serialNumber =
|
||||
this.itemSerialNumbers[item.item as string] ?? undefined;
|
||||
}
|
||||
|
||||
shipmentDoc.once('afterSubmit', () => {
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: t`Shipment ${shipmentDoc.name as string} is Submitted`,
|
||||
duration: 'short',
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
await shipmentDoc.sync();
|
||||
await shipmentDoc.submit();
|
||||
} catch (error) {
|
||||
return showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
async submitSinvDoc(shouldPrint: boolean) {
|
||||
this.sinvDoc.once('afterSubmit', async () => {
|
||||
showToast({
|
||||
type: 'success',
|
||||
message: t`Sales Invoice ${this.sinvDoc.name as string} is Submitted`,
|
||||
duration: 'short',
|
||||
});
|
||||
|
||||
if (shouldPrint) {
|
||||
await routeTo(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`/print/${this.sinvDoc.schemaName}/${this.sinvDoc.name}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await this.validate();
|
||||
await this.sinvDoc.runFormulas();
|
||||
await this.sinvDoc.sync();
|
||||
await this.sinvDoc.submit();
|
||||
} catch (error) {
|
||||
return showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async afterTransaction() {
|
||||
await this.setItemQtyMap();
|
||||
this.clearValues();
|
||||
this.setSinvDoc();
|
||||
this.toggleModal('Payment', false);
|
||||
},
|
||||
clearValues() {
|
||||
this.setSinvDoc();
|
||||
this.itemSerialNumbers = {};
|
||||
|
||||
this.cashAmount = fyo.pesa(0);
|
||||
this.transferAmount = fyo.pesa(0);
|
||||
},
|
||||
toggleModal(modal: ModalName, value?: boolean) {
|
||||
if (value) {
|
||||
return (this[`open${modal}Modal`] = value);
|
||||
}
|
||||
return (this[`open${modal}Modal`] = !this[`open${modal}Modal`]);
|
||||
},
|
||||
updateValues() {
|
||||
this.setTotalQuantity();
|
||||
this.setItemDiscounts();
|
||||
this.setTotalTaxedAmount();
|
||||
},
|
||||
async validate() {
|
||||
validateSinv(this.sinvDoc as SalesInvoice, this.itemQtyMap);
|
||||
await validateShipment(this.itemSerialNumbers);
|
||||
},
|
||||
|
||||
getItem,
|
||||
},
|
||||
});
|
||||
</script>
|
325
src/pages/POS/PaymentModal.vue
Normal file
325
src/pages/POS/PaymentModal.vue
Normal file
@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<Modal class="w-2/6 ml-auto mr-3.5" :set-close-listener="false">
|
||||
<div v-if="sinvDoc.fieldMap" class="px-4 py-6 grid" style="height: 95vh">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<Currency
|
||||
:df="fyo.fieldMap.PaymentFor.amount"
|
||||
:read-only="!transferAmount.isZero()"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="cashAmount"
|
||||
@change="(amount:Money)=> $emit('setCashAmount', amount)"
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="w-full py-5 bg-teal-500"
|
||||
@click="setCashOrTransferAmount"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cash` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Currency
|
||||
:df="fyo.fieldMap.PaymentFor.amount"
|
||||
:read-only="!cashAmount.isZero()"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="transferAmount"
|
||||
@change="(value:Money)=> $emit('setTransferAmount', value)"
|
||||
/>
|
||||
|
||||
<Button
|
||||
class="w-full py-5 bg-teal-500"
|
||||
@click="setCashOrTransferAmount('Transfer')"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Transfer` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid grid-cols-2 gap-6">
|
||||
<Data
|
||||
v-show="!transferAmount.isZero()"
|
||||
:df="fyo.fieldMap.Payment.referenceId"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:required="!transferAmount.isZero()"
|
||||
:value="transferRefNo"
|
||||
@change="(value:string) => $emit('setTransferRefNo', value)"
|
||||
/>
|
||||
|
||||
<Date
|
||||
v-show="!transferAmount.isZero()"
|
||||
:df="fyo.fieldMap.Payment.clearanceDate"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:required="!transferAmount.isZero()"
|
||||
:value="transferClearanceDate"
|
||||
@change="(value:Date) => $emit('setTransferClearanceDate', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-14 grid grid-cols-2 gap-6">
|
||||
<Currency
|
||||
v-show="showPaidChange"
|
||||
:df="{
|
||||
label: t`Paid Change`,
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'paidChange',
|
||||
}"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="paidChange"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
v-show="showBalanceAmount"
|
||||
:df="{
|
||||
label: t`Balance Amount`,
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'balanceAmount',
|
||||
}"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="balanceAmount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mb-14 row-start-4 row-span-2 grid grid-cols-2 gap-x-6 gap-y-11"
|
||||
>
|
||||
<Currency
|
||||
:df="sinvDoc.fieldMap.netTotal"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="sinvDoc?.netTotal"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="{
|
||||
label: t`Taxes and Charges`,
|
||||
fieldtype: 'Currency',
|
||||
fieldname: 'taxesAndCharges',
|
||||
}"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="totalTaxedAmount"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="sinvDoc.fieldMap.baseGrandTotal"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="sinvDoc?.baseGrandTotal"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
v-if="isDiscountingEnabled"
|
||||
:df="sinvDoc.fieldMap.discountAmount"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="itemDiscounts"
|
||||
/>
|
||||
|
||||
<Currency
|
||||
:df="sinvDoc.fieldMap.grandTotal"
|
||||
:read-only="true"
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:text-right="true"
|
||||
:value="sinvDoc?.grandTotal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row-start-6 grid grid-cols-2 gap-4 mt-auto">
|
||||
<div class="col-span-2">
|
||||
<Button
|
||||
class="w-full bg-red-500"
|
||||
style="padding: 1.35rem"
|
||||
@click="$emit('toggleModal', 'Payment')"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Cancel` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<Button
|
||||
class="w-full bg-blue-500"
|
||||
style="padding: 1.35rem"
|
||||
:disabled="disableSubmitButton"
|
||||
@click="$emit('createTransaction')"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Button
|
||||
class="w-full bg-green-500"
|
||||
style="padding: 1.35rem"
|
||||
:disabled="disableSubmitButton"
|
||||
@click="$emit('createTransaction', true)"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit & Print` }}
|
||||
</p>
|
||||
</slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Currency from 'src/components/Controls/Currency.vue';
|
||||
import Data from 'src/components/Controls/Data.vue';
|
||||
import Date from 'src/components/Controls/Date.vue';
|
||||
import Modal from 'src/components/Modal.vue';
|
||||
import { Money } from 'pesa';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PaymentModal',
|
||||
components: {
|
||||
Modal,
|
||||
Currency,
|
||||
Button,
|
||||
Data,
|
||||
Date,
|
||||
},
|
||||
emits: [
|
||||
'createTransaction',
|
||||
'setCashAmount',
|
||||
'setTransferAmount',
|
||||
'setTransferClearanceDate',
|
||||
'setTransferRefNo',
|
||||
'toggleModal',
|
||||
],
|
||||
setup() {
|
||||
return {
|
||||
cashAmount: inject('cashAmount') as Money,
|
||||
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
|
||||
itemDiscounts: inject('itemDiscounts') as Money,
|
||||
transferAmount: inject('transferAmount') as Money,
|
||||
sinvDoc: inject('sinvDoc') as SalesInvoice,
|
||||
transferRefNo: inject('transferRefNo') as string,
|
||||
transferClearanceDate: inject('transferClearanceDate') as Date,
|
||||
totalTaxedAmount: inject('totalTaxedAmount') as Money,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
balanceAmount(): Money {
|
||||
const grandTotal = this.sinvDoc?.grandTotal ?? fyo.pesa(0);
|
||||
|
||||
if (this.cashAmount.isZero()) {
|
||||
return grandTotal.sub(this.transferAmount);
|
||||
}
|
||||
|
||||
return grandTotal.sub(this.cashAmount);
|
||||
},
|
||||
paidChange(): Money {
|
||||
const grandTotal = this.sinvDoc?.grandTotal ?? fyo.pesa(0);
|
||||
|
||||
if (this.cashAmount.isZero()) {
|
||||
return this.transferAmount.sub(grandTotal);
|
||||
}
|
||||
|
||||
return this.cashAmount.sub(grandTotal);
|
||||
},
|
||||
showBalanceAmount(): boolean {
|
||||
if (
|
||||
this.cashAmount.eq(fyo.pesa(0)) &&
|
||||
this.transferAmount.eq(fyo.pesa(0))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cashAmount.gte(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.transferAmount.gte(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
showPaidChange(): boolean {
|
||||
if (
|
||||
this.cashAmount.eq(fyo.pesa(0)) &&
|
||||
this.transferAmount.eq(fyo.pesa(0))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cashAmount.gt(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.transferAmount.gt(this.sinvDoc?.grandTotal ?? fyo.pesa(0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
disableSubmitButton(): boolean {
|
||||
if (
|
||||
!this.sinvDoc.grandTotal?.isZero() &&
|
||||
this.transferAmount.isZero() &&
|
||||
this.cashAmount.isZero()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.cashAmount.isZero() &&
|
||||
(!this.transferRefNo || !this.transferClearanceDate)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setCashOrTransferAmount(paymentMethod = 'Cash') {
|
||||
if (paymentMethod === 'Transfer') {
|
||||
this.$emit('setCashAmount', fyo.pesa(0));
|
||||
this.$emit('setTransferAmount', this.sinvDoc?.grandTotal);
|
||||
return;
|
||||
}
|
||||
this.$emit('setTransferAmount', fyo.pesa(0));
|
||||
this.$emit('setCashAmount', this.sinvDoc?.grandTotal);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@ -115,6 +115,7 @@ export default defineComponent({
|
||||
ModelNameEnum.AccountingSettings,
|
||||
ModelNameEnum.InventorySettings,
|
||||
ModelNameEnum.Defaults,
|
||||
ModelNameEnum.POSSettings,
|
||||
ModelNameEnum.PrintSettings,
|
||||
ModelNameEnum.SystemSettings,
|
||||
].some((s) => this.fyo.singles[s]?.canSave);
|
||||
@ -133,6 +134,7 @@ export default defineComponent({
|
||||
[ModelNameEnum.PrintSettings]: this.t`Print`,
|
||||
[ModelNameEnum.InventorySettings]: this.t`Inventory`,
|
||||
[ModelNameEnum.Defaults]: this.t`Defaults`,
|
||||
[ModelNameEnum.POSSettings]: this.t`POS Settings`,
|
||||
[ModelNameEnum.SystemSettings]: this.t`System`,
|
||||
};
|
||||
},
|
||||
@ -140,16 +142,26 @@ export default defineComponent({
|
||||
const enableInventory =
|
||||
!!this.fyo.singles.AccountingSettings?.enableInventory;
|
||||
|
||||
const enablePOS = !!this.fyo.singles.InventorySettings?.enablePointOfSale;
|
||||
|
||||
return [
|
||||
ModelNameEnum.AccountingSettings,
|
||||
ModelNameEnum.InventorySettings,
|
||||
ModelNameEnum.Defaults,
|
||||
ModelNameEnum.POSSettings,
|
||||
ModelNameEnum.PrintSettings,
|
||||
ModelNameEnum.SystemSettings,
|
||||
]
|
||||
.filter((s) =>
|
||||
s === ModelNameEnum.InventorySettings ? enableInventory : true
|
||||
)
|
||||
.filter((s) => {
|
||||
if (s === ModelNameEnum.InventorySettings && !enableInventory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s === ModelNameEnum.POSSettings && !enablePOS) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((s) => this.fyo.schemaMap[s]!);
|
||||
},
|
||||
activeGroup(): Map<string, Field[]> {
|
||||
|
67
src/pages/TemplateBuilder/SetType.vue
Normal file
67
src/pages/TemplateBuilder/SetType.vue
Normal 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>
|
@ -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`,
|
||||
|
@ -11,6 +11,7 @@ import Report from 'src/pages/Report.vue';
|
||||
import Settings from 'src/pages/Settings/Settings.vue';
|
||||
import TemplateBuilder from 'src/pages/TemplateBuilder/TemplateBuilder.vue';
|
||||
import CustomizeForm from 'src/pages/CustomizeForm/CustomizeForm.vue';
|
||||
import POS from 'src/pages/POS/POS.vue';
|
||||
import type { HistoryState } from 'vue-router';
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||
import { historyState } from './utils/refs';
|
||||
@ -124,6 +125,18 @@ const routes: RouteRecordRaw[] = [
|
||||
edit: (route) => route.query,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/pos',
|
||||
name: 'Point of Sale',
|
||||
components: {
|
||||
default: POS,
|
||||
edit: QuickEditForm,
|
||||
},
|
||||
props: {
|
||||
default: true,
|
||||
edit: (route) => route.query,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({ routes, history: createWebHistory() });
|
||||
|
@ -5,20 +5,21 @@ import { systemLanguageRef } from './refs';
|
||||
|
||||
// Language: Language Code in books/translations
|
||||
export const languageCodeMap: Record<string, string> = {
|
||||
Arabic: 'ar',
|
||||
Catalan: 'ca-ES',
|
||||
Danish: 'da',
|
||||
Dutch: 'nl',
|
||||
English: 'en',
|
||||
French: 'fr',
|
||||
German: 'de',
|
||||
Portuguese: 'pt',
|
||||
Arabic: 'ar',
|
||||
Catalan: 'ca-ES',
|
||||
Spanish: 'es',
|
||||
Dutch: 'nl',
|
||||
Gujarati: 'gu',
|
||||
Turkish: 'tr',
|
||||
Korean: 'ko',
|
||||
Swedish: 'sv',
|
||||
Danish: 'da',
|
||||
Nepali: 'np',
|
||||
Portuguese: 'pt',
|
||||
'Simplified Chinese': 'zh-CN',
|
||||
Spanish: 'es',
|
||||
Swedish: 'sv',
|
||||
Turkish: 'tr',
|
||||
};
|
||||
|
||||
export async function setLanguageMap(
|
||||
|
294
src/utils/pos.ts
Normal file
294
src/utils/pos.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { AccountTypeEnum } from 'models/baseModels/Account/types';
|
||||
import { Item } from 'models/baseModels/Item/Item';
|
||||
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
|
||||
import { SalesInvoiceItem } from 'models/baseModels/SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { POSShift } from 'models/inventory/Point of Sale/POSShift';
|
||||
import { ValuationMethod } from 'models/inventory/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import {
|
||||
getRawStockLedgerEntries,
|
||||
getStockBalanceEntries,
|
||||
getStockLedgerEntries,
|
||||
} from 'reports/inventory/helpers';
|
||||
import { ItemQtyMap, ItemSerialNumbers } from 'src/components/POS/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { safeParseFloat } from 'utils/index';
|
||||
import { showToast } from './interactive';
|
||||
|
||||
export async function getItemQtyMap(): Promise<ItemQtyMap> {
|
||||
const itemQtyMap: ItemQtyMap = {};
|
||||
const valuationMethod =
|
||||
fyo.singles.InventorySettings?.valuationMethod ?? ValuationMethod.FIFO;
|
||||
|
||||
const rawSLEs = await getRawStockLedgerEntries(fyo);
|
||||
const rawData = getStockLedgerEntries(rawSLEs, valuationMethod);
|
||||
const posInventory = fyo.singles.POSSettings?.inventory;
|
||||
|
||||
const stockBalance = getStockBalanceEntries(rawData, {
|
||||
location: posInventory,
|
||||
});
|
||||
|
||||
for (const row of stockBalance) {
|
||||
if (!itemQtyMap[row.item]) {
|
||||
itemQtyMap[row.item] = { availableQty: 0 };
|
||||
}
|
||||
|
||||
if (row.batch) {
|
||||
itemQtyMap[row.item][row.batch] = row.balanceQuantity;
|
||||
}
|
||||
|
||||
itemQtyMap[row.item].availableQty += row.balanceQuantity;
|
||||
}
|
||||
return itemQtyMap;
|
||||
}
|
||||
|
||||
export function getTotalQuantity(items: SalesInvoiceItem[]): number {
|
||||
let totalQuantity = safeParseFloat(0);
|
||||
|
||||
if (!items.length) {
|
||||
return totalQuantity;
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const quantity = item.quantity ?? 0;
|
||||
totalQuantity = safeParseFloat(totalQuantity + quantity);
|
||||
}
|
||||
return totalQuantity;
|
||||
}
|
||||
|
||||
export function getItemDiscounts(items: SalesInvoiceItem[]): Money {
|
||||
let itemDiscounts = fyo.pesa(0);
|
||||
|
||||
if (!items.length) {
|
||||
return itemDiscounts;
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
if (!item.itemDiscountAmount?.isZero()) {
|
||||
itemDiscounts = itemDiscounts.add(item.itemDiscountAmount as Money);
|
||||
}
|
||||
|
||||
if (item.amount && (item.itemDiscountPercent as number) > 1) {
|
||||
itemDiscounts = itemDiscounts.add(
|
||||
item.amount.percent(item.itemDiscountPercent as number)
|
||||
);
|
||||
}
|
||||
}
|
||||
return itemDiscounts;
|
||||
}
|
||||
|
||||
export async function getItem(item: string): Promise<Item | undefined> {
|
||||
const itemDoc = (await fyo.doc.getDoc(ModelNameEnum.Item, item)) as Item;
|
||||
if (!itemDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
return itemDoc;
|
||||
}
|
||||
|
||||
export function validateSinv(sinvDoc: SalesInvoice, itemQtyMap: ItemQtyMap) {
|
||||
if (!sinvDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateSinvItems(sinvDoc.items as SalesInvoiceItem[], itemQtyMap);
|
||||
}
|
||||
|
||||
function validateSinvItems(
|
||||
sinvItems: SalesInvoiceItem[],
|
||||
itemQtyMap: ItemQtyMap
|
||||
) {
|
||||
for (const item of sinvItems) {
|
||||
if (!item.quantity || item.quantity < 1) {
|
||||
throw new ValidationError(
|
||||
t`Invalid Quantity for Item ${item.item as string}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!itemQtyMap[item.item as string]) {
|
||||
throw new ValidationError(t`Item ${item.item as string} not in Stock`);
|
||||
}
|
||||
|
||||
if (item.quantity > itemQtyMap[item.item as string].availableQty) {
|
||||
throw new ValidationError(
|
||||
t`Insufficient Quantity. Item ${item.item as string} has only ${
|
||||
itemQtyMap[item.item as string].availableQty
|
||||
} quantities available. you selected ${item.quantity}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateShipment(itemSerialNumbers: ItemSerialNumbers) {
|
||||
if (!itemSerialNumbers) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const idx in itemSerialNumbers) {
|
||||
const serialNumbers = itemSerialNumbers[idx].split('\n');
|
||||
|
||||
for (const serialNumber of serialNumbers) {
|
||||
const status = await fyo.getValue(
|
||||
ModelNameEnum.SerialNumber,
|
||||
serialNumber,
|
||||
'status'
|
||||
);
|
||||
|
||||
if (status !== 'Active') {
|
||||
throw new ValidationError(
|
||||
t`Serial Number ${serialNumber} status is not Active.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateIsPosSettingsSet(fyo: Fyo) {
|
||||
try {
|
||||
const inventory = fyo.singles.POSSettings?.inventory;
|
||||
if (!inventory) {
|
||||
throw new ValidationError(
|
||||
t`POS Inventory is not set. Please set it on POS Settings`
|
||||
);
|
||||
}
|
||||
|
||||
const cashAccount = fyo.singles.POSSettings?.cashAccount;
|
||||
if (!cashAccount) {
|
||||
throw new ValidationError(
|
||||
t`POS Counter Cash Account is not set. Please set it on POS Settings`
|
||||
);
|
||||
}
|
||||
|
||||
const writeOffAccount = fyo.singles.POSSettings?.writeOffAccount;
|
||||
if (!writeOffAccount) {
|
||||
throw new ValidationError(
|
||||
t`POS Write Off Account is not set. Please set it on POS Settings`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: t`${error as string}`,
|
||||
duration: 'long',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getTotalTaxedAmount(sinvDoc: SalesInvoice): Money {
|
||||
let totalTaxedAmount = fyo.pesa(0);
|
||||
if (!sinvDoc.items?.length || !sinvDoc.taxes?.length) {
|
||||
return totalTaxedAmount;
|
||||
}
|
||||
|
||||
for (const row of sinvDoc.taxes) {
|
||||
totalTaxedAmount = totalTaxedAmount.add(row.amount as Money);
|
||||
}
|
||||
return totalTaxedAmount;
|
||||
}
|
||||
|
||||
export function validateClosingAmounts(posShiftDoc: POSShift) {
|
||||
try {
|
||||
if (!posShiftDoc) {
|
||||
throw new ValidationError(
|
||||
`POS Shift Document not loaded. Please reload.`
|
||||
);
|
||||
}
|
||||
|
||||
posShiftDoc.closingAmounts?.map((row) => {
|
||||
if (row.closingAmount?.isNegative()) {
|
||||
throw new ValidationError(
|
||||
t`Closing ${row.paymentMethod as string} Amount can not be negative.`
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
export async function transferPOSCashAndWriteOff(
|
||||
fyo: Fyo,
|
||||
posShiftDoc: POSShift
|
||||
) {
|
||||
const expectedCashAmount = posShiftDoc.closingAmounts?.find(
|
||||
(row) => row.paymentMethod === 'Cash'
|
||||
)?.expectedAmount as Money;
|
||||
|
||||
if (expectedCashAmount.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closingCashAmount = posShiftDoc.closingAmounts?.find(
|
||||
(row) => row.paymentMethod === 'Cash'
|
||||
)?.closingAmount as Money;
|
||||
|
||||
const jvDoc = fyo.doc.getNewDoc(ModelNameEnum.JournalEntry, {
|
||||
entryType: 'Journal Entry',
|
||||
});
|
||||
|
||||
await jvDoc.append('accounts', {
|
||||
account: AccountTypeEnum.Cash,
|
||||
debit: closingCashAmount,
|
||||
});
|
||||
|
||||
await jvDoc.append('accounts', {
|
||||
account: fyo.singles.POSSettings?.cashAccount,
|
||||
credit: closingCashAmount,
|
||||
});
|
||||
|
||||
const differenceAmount = posShiftDoc?.closingAmounts?.find(
|
||||
(row) => row.paymentMethod === 'Cash'
|
||||
)?.differenceAmount as Money;
|
||||
|
||||
if (differenceAmount.isNegative()) {
|
||||
await jvDoc.append('accounts', {
|
||||
account: AccountTypeEnum.Cash,
|
||||
debit: differenceAmount.abs(),
|
||||
credit: fyo.pesa(0),
|
||||
});
|
||||
await jvDoc.append('accounts', {
|
||||
account: fyo.singles.POSSettings?.writeOffAccount,
|
||||
debit: fyo.pesa(0),
|
||||
credit: differenceAmount.abs(),
|
||||
});
|
||||
}
|
||||
|
||||
if (!differenceAmount.isZero() && differenceAmount.isPositive()) {
|
||||
await jvDoc.append('accounts', {
|
||||
account: fyo.singles.POSSettings?.writeOffAccount,
|
||||
debit: differenceAmount,
|
||||
credit: fyo.pesa(0),
|
||||
});
|
||||
await jvDoc.append('accounts', {
|
||||
account: AccountTypeEnum.Cash,
|
||||
debit: fyo.pesa(0),
|
||||
credit: differenceAmount,
|
||||
});
|
||||
}
|
||||
|
||||
await (await jvDoc.sync()).submit();
|
||||
}
|
||||
|
||||
export function validateSerialNumberCount(
|
||||
serialNumbers: string | undefined,
|
||||
quantity: number,
|
||||
item: string
|
||||
) {
|
||||
let serialNumberCount = 0;
|
||||
|
||||
if (serialNumbers) {
|
||||
serialNumberCount = serialNumbers.split('\n').length;
|
||||
}
|
||||
|
||||
if (quantity !== serialNumberCount) {
|
||||
const errorMessage = t`Need ${quantity} Serial Numbers for Item ${item}. You have provided ${serialNumberCount}`;
|
||||
|
||||
showToast({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
duration: 'long',
|
||||
});
|
||||
throw new ValidationError(errorMessage);
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ const printSettingsFields = [
|
||||
'address',
|
||||
'companyName',
|
||||
];
|
||||
const accountingSettingsFields = ['gstin'];
|
||||
const accountingSettingsFields = ['gstin', 'taxId'];
|
||||
|
||||
export async function getPrintTemplatePropValues(
|
||||
doc: Doc
|
||||
@ -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) => {
|
||||
const label = fyo.schemaMap[schemaName]?.label ?? schemaName;
|
||||
return { name: `${name} - ${label}`, type: 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">
|
||||
|
@ -101,6 +101,20 @@ function getInventorySidebar(): SidebarRoot[] {
|
||||
];
|
||||
}
|
||||
|
||||
function getPOSSidebar() {
|
||||
const isPOSEnabled = !!fyo.singles.InventorySettings?.enablePointOfSale;
|
||||
if (!isPOSEnabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return {
|
||||
label: t`POS`,
|
||||
name: 'pos',
|
||||
route: '/pos',
|
||||
icon: 'pos',
|
||||
};
|
||||
}
|
||||
|
||||
function getReportSidebar() {
|
||||
return {
|
||||
label: t`Reports`,
|
||||
@ -155,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',
|
||||
@ -256,6 +276,7 @@ function getCompleteSidebar(): SidebarConfig {
|
||||
},
|
||||
getReportSidebar(),
|
||||
getInventorySidebar(),
|
||||
getPOSSidebar(),
|
||||
getRegionalSidebar(),
|
||||
{
|
||||
label: t`Setup`,
|
||||
|
@ -1,22 +1,23 @@
|
||||
${0},,
|
||||
"${0} ${1} already exists.","${0} ${1} موجودة من قبل.",
|
||||
"${0} ${1} does not exist",,
|
||||
"${0} ${1} has been modified after loading please reload entry.",,
|
||||
"${0} ${1} does not exist","${0} ${1} غير موجود",
|
||||
"${0} ${1} has been modified after loading please reload entry.","${0} ${1} تم تعديله بعد التحميل يرجى إعادة تحميل الإدخال.",
|
||||
"${0} ${1} is linked with existing records.","${0} ${1} مرطبت بسجل موجود.",
|
||||
"${0} account not set in Inventory Settings.",,
|
||||
"${0} already saved",,
|
||||
"${0} already submitted",,
|
||||
"${0} cancelled",,
|
||||
"${0} cannot be cancelled",,
|
||||
"${0} cannot be deleted",,
|
||||
"${0} deleted",,
|
||||
"${0} entries failed",,
|
||||
"${0} entries imported",,
|
||||
"${0} entry failed",,
|
||||
"${0} entry imported",,
|
||||
"${0} fields selected",,
|
||||
"${0} account not set in Inventory Settings.","${0} لم يتم تعيين الحساب في إعدادات المخزون.",
|
||||
"${0} already saved","${0} تم حفظ بالفعل",
|
||||
"${0} already submitted","${0} تم الارسال بالفعل",
|
||||
"${0} cancelled","${0} تم الالغاء",
|
||||
"${0} cannot be cancelled","${0} لا يمكن الالغاء",
|
||||
"${0} cannot be deleted","${0} لا يمكن الحذف",
|
||||
"${0} deleted","${0} تم الحذف",
|
||||
"${0} entries failed","${0} فشل الادخال",
|
||||
"${0} entries imported","${0} الإدخالات المستوردة",
|
||||
"${0} entry failed","${0} فشل الإدخال",
|
||||
"${0} entry imported","${0} الإدخال مستورد",
|
||||
"${0} fields selected","${0} الحقول المحددة",
|
||||
"${0} filters applied","${0} تم تطبيق الفيلتر",
|
||||
"${0} has linked child accounts.",,
|
||||
"${0} of type ${1} does not exist",,
|
||||
"${0} has linked child accounts.","${0} لديه حسابات فرعية مرتبطة.",
|
||||
"${0} of type ${1} does not exist","${0} من النوع ${1} غير موجود",
|
||||
"${0} out of ${1}",,
|
||||
"${0} party ${1} is different from ${2}",,
|
||||
"${0} quantity 1 added.",,
|
||||
@ -70,6 +71,7 @@ Active,,
|
||||
"Add products or services that you buy from your suppliers",,
|
||||
"Add products or services that you sell to your customers",,
|
||||
"Add transfer terms",,
|
||||
"Add'l Discounts",,
|
||||
"Additional ${0} Serial Numbers required for ${1} quantity of ${2}.",,
|
||||
"Additional quantity (${0}) required${1} to make outward transfer of item ${2} from ${3} on ${4}",,
|
||||
Address,العنوان,
|
||||
@ -105,9 +107,11 @@ August,,
|
||||
"Auto Payments"," خاصية الدفع التلقائي",
|
||||
"Auto Stock Transfer"," خاصية نقل المنتجات تلقائيًا بين المخازن",
|
||||
Autocomplete,,
|
||||
Back,,
|
||||
"Back Reference",,
|
||||
"Bad import data, could not read file.",,
|
||||
Balance,التوازن,
|
||||
"Balance Amount",,
|
||||
"Balance Sheet","تقرير الميزانية",
|
||||
Bank,,
|
||||
"Bank Accounts","حسابات بنكية",
|
||||
@ -138,6 +142,7 @@ Cancelled,ملغاة,
|
||||
"Cannot Import",,
|
||||
"Cannot Open File",,
|
||||
"Cannot cancel ${0} ${1} because of the following ${2}: ${3}",,
|
||||
"Cannot cancel ${0} because of the following ${1}: ${2}",,
|
||||
"Cannot delete ${0} ""${1}"" because of linked entries.",,
|
||||
"Cannot open file",,
|
||||
"Cannot perform operation.",,
|
||||
@ -145,6 +150,7 @@ Cancelled,ملغاة,
|
||||
"Capital Equipments","معدات رأس المال",
|
||||
"Capital Stock","رأس المال",
|
||||
Cash,نقد,
|
||||
"Cash Denominations",,
|
||||
"Cash Entry","قيود النقدية",
|
||||
"Cash In Hand","نقدا في اليد",
|
||||
Cashflow,"التدفق النقدي",
|
||||
@ -163,10 +169,15 @@ Clear,واضح,
|
||||
"Clearance Date","تاريخ التخليص",
|
||||
Close,إغلاق,
|
||||
"Close Frappe Books and try manually.",,
|
||||
"Close POS Shift",,
|
||||
"Close Quick Search",,
|
||||
Closing,اغلاق,
|
||||
"Closing ${0} Amount can not be negative.",,
|
||||
"Closing (Cr)","(Cr) إغلاق",
|
||||
"Closing (Dr)","(Dr) إغلاق",
|
||||
"Closing Amount",,
|
||||
"Closing Cash In Denominations",,
|
||||
"Closing Date",,
|
||||
Collapse,,
|
||||
Color,اللون,
|
||||
"Commission on Sales","عمولة على المبيعات",
|
||||
@ -187,6 +198,8 @@ Contains,يحتوي,
|
||||
"Cost Of Goods Sold Acc."," حساب تكلفة البضاعة المباعة ",
|
||||
"Cost of Goods Sold","تكلفة البضائع المباعة",
|
||||
"Could not connect to database file ${0}, please select the file manually",,
|
||||
Count,,
|
||||
"Counter Cash Account",,
|
||||
Country,البلد,
|
||||
"Country Code","كود البلد",
|
||||
"Country code used to initialize regional settings.","كود البلد يستخدم لتثبت اعدادات الاقليمية",
|
||||
@ -246,6 +259,7 @@ December,,
|
||||
"Decrease print template display scale",,
|
||||
Default,,
|
||||
"Default Account","الحساب الافتراضي",
|
||||
"Default Cash Denominations",,
|
||||
"Default Location"," الموقع الافتراضي",
|
||||
Defaults,"الاعدادات الافتراضية ",
|
||||
Delete,حذف,
|
||||
@ -254,10 +268,12 @@ Delete,حذف,
|
||||
"Delete Failed",,
|
||||
"Delete Group",,
|
||||
Delivered,,
|
||||
Denomination,,
|
||||
Depreciation,إهلاك,
|
||||
"Depreciation Entry","ادخال قيد الاهلاك",
|
||||
Description,الوصف,
|
||||
Details,التفاصيل,
|
||||
"Difference Amount",,
|
||||
"Direct Expenses","المصاريف المباشرة",
|
||||
"Direct Income","الدخل المباشر",
|
||||
"Directory for database file ${0} does not exist, please select the file manually",,
|
||||
@ -301,6 +317,8 @@ Empty,فارغ,
|
||||
"Enable Discount Accounting"," تفعيل حساب الخصم",
|
||||
"Enable Form Customization",,
|
||||
"Enable Inventory"," تفعيل المخزون",
|
||||
"Enable Invoice Returns",,
|
||||
"Enable Point of Sale",,
|
||||
"Enable Price List"," تفعيل قائمة الاسعار",
|
||||
"Enable Serial Number"," تفعيل السريال نمبر",
|
||||
"Enable Stock Returns",,
|
||||
@ -325,6 +343,7 @@ Error,خطأ,
|
||||
"Excise Entry",,
|
||||
"Existing Company",,
|
||||
Expand,,
|
||||
"Expected Amount",,
|
||||
Expense,المصاريف,
|
||||
"Expense Account","حساب المصروف",
|
||||
Expenses,المصروفات,
|
||||
@ -335,7 +354,6 @@ Export,تصدير,
|
||||
"Export Format",,
|
||||
"Export Successful","تم التصدير بنجاح",
|
||||
"Export Wizard",,
|
||||
FIFO,,
|
||||
Failed,,
|
||||
Fax,فاكس,
|
||||
Features," الخصائص",
|
||||
@ -427,10 +445,12 @@ Inflow,التدفق,
|
||||
"Instance Id",,
|
||||
"Insufficient Quantity",,
|
||||
"Insufficient Quantity.",,
|
||||
"Insufficient Quantity. Item ${0} has only ${1} quantities available. you selected ${2}",,
|
||||
Int,,
|
||||
"Intergrated Tax","ضريبة متكاملة",
|
||||
"Internal Precision","الدقة الداخلية",
|
||||
"Invalid Key Error",,
|
||||
"Invalid Quantity for Item ${0}",,
|
||||
"Invalid barcode value ${0}.",,
|
||||
"Invalid value ${0} for ${1}",,
|
||||
"Invalid value found for ${0}",,
|
||||
@ -452,13 +472,17 @@ Is,هو,
|
||||
"Is Landscape",,
|
||||
"Is Not",ليس,
|
||||
"Is Not Empty","ليس فارغ",
|
||||
"Is POS Shift Open",,
|
||||
"Is Price List Enabled",,
|
||||
"Is Required",,
|
||||
"Is Whole",,
|
||||
Item,صنف,
|
||||
"Item ${0} has Zero Quantity",,
|
||||
"Item ${0} is a batched item",,
|
||||
"Item ${0} is not a batched item",,
|
||||
"Item ${0} not in Stock",,
|
||||
"Item Description",,
|
||||
"Item Discounts",,
|
||||
"Item Name","اسم العنصر",
|
||||
"Item Prices",,
|
||||
"Item with From location not found",,
|
||||
@ -527,9 +551,9 @@ More,,
|
||||
"More Filters",,
|
||||
"More shortcuts will be added soon.",,
|
||||
"Movement Type"," نوع التنقل ",
|
||||
"Moving Average",,
|
||||
Name,الاسم,
|
||||
Navigate,,
|
||||
"Need ${0} Serial Numbers for Item ${1}. You have provided ${2}",,
|
||||
"Net Total","الإجمالي الصافي",
|
||||
"New ${0}",,
|
||||
"New ${0} ${1}",,
|
||||
@ -582,8 +606,12 @@ Okay,,
|
||||
"Open the Export Wizard modal",,
|
||||
"Opening (Cr)","(Cr) الافتتاح",
|
||||
"Opening (Dr)","(Dr) افتتاح",
|
||||
"Opening Amount",,
|
||||
"Opening Balance Equity","الرصيد الافتتاحي حقوق الملكية",
|
||||
"Opening Balances","أرصدة الافتتاح",
|
||||
"Opening Cash Amount can not be negative.",,
|
||||
"Opening Cash In Denominations",,
|
||||
"Opening Date",,
|
||||
"Opening Entry",,
|
||||
Options,,
|
||||
Orange,,
|
||||
@ -591,10 +619,18 @@ Organisation,التنظيم,
|
||||
Outflow,التدفق,
|
||||
Outstanding,,
|
||||
"Outstanding Amount","المبلغ المستحق",
|
||||
POS,,
|
||||
"POS Counter Cash Account is not set. Please set it on POS Settings",,
|
||||
"POS Customer",,
|
||||
"POS Inventory is not set. Please set it on POS Settings",,
|
||||
"POS Settings",,
|
||||
"POS Shift Amount",,
|
||||
"POS Write Off Account is not set. Please set it on POS Settings",,
|
||||
"Pad Zeros",,
|
||||
Page,,
|
||||
Paid,مدفوع,
|
||||
"Paid ${0}",,
|
||||
"Paid Change",,
|
||||
Parent,الوالد,
|
||||
"Parent Account","الحساب الوالد",
|
||||
Party,"مورد / عميل",
|
||||
@ -602,6 +638,7 @@ Party,"مورد / عميل",
|
||||
Pay,دفع,
|
||||
Payable,,
|
||||
Payment,الدفع,
|
||||
"Payment ${0} is Saved",,
|
||||
"Payment For","الدفع مقابل",
|
||||
"Payment Method","طريقة الدفع",
|
||||
"Payment No","رقم الدفع",
|
||||
@ -637,6 +674,7 @@ Place,مكان,
|
||||
"Please set GSTIN in General Settings.",,
|
||||
"Please set Round Off Account in the Settings.",,
|
||||
"Please set a Display Doc",,
|
||||
"Point of Sale",,
|
||||
"Postal Code","الرمز البريدي",
|
||||
"Postal Expenses","المصاريف البريدية",
|
||||
"Posting Date","تاريخ النشر",
|
||||
@ -680,6 +718,7 @@ Purchase,شراء,
|
||||
Purchases,المشتريات,
|
||||
Purple,,
|
||||
Purpose,,
|
||||
"Qty in Batch",,
|
||||
"Qty. ${0}",,
|
||||
"Qty. in Transfer Unit",,
|
||||
Quantity,الكمية,
|
||||
@ -689,6 +728,8 @@ Quarterly,,
|
||||
Quarters,,
|
||||
"Quick Search",,
|
||||
"Quick edit error: ${0} entry has no name.",,
|
||||
Quote,,
|
||||
"Quote Reference",,
|
||||
Rate,معدل,
|
||||
"Rate (${0}) cannot be less zero.","لا يمكن أن يكون السعر (${0}) أقل من صفر.",
|
||||
"Rate (${0}) has to be greater than zero",,
|
||||
@ -739,6 +780,7 @@ Sales,المبيعات,
|
||||
"Sales Acc."," حساب المبيعات",
|
||||
"Sales Expenses","مصاريف المبيعات",
|
||||
"Sales Invoice","فاتورة مبيعات",
|
||||
"Sales Invoice ${0} is Submitted",,
|
||||
"Sales Invoice Item","اصناف فاتورة المبيعات",
|
||||
"Sales Invoice Number Series"," تسلسل فاتورة المبيعات",
|
||||
"Sales Invoice Print Template"," قالب الطابعة فاتورة المبيعات ",
|
||||
@ -750,6 +792,11 @@ Sales,المبيعات,
|
||||
"Sales Payment",,
|
||||
"Sales Payment Account"," حساب دفع المبيعات",
|
||||
"Sales Payments","مدفوعات المبيعات",
|
||||
"Sales Quote",,
|
||||
"Sales Quote Item",,
|
||||
"Sales Quote Number Series",,
|
||||
"Sales Quote Print Template",,
|
||||
"Sales Quotes",,
|
||||
"Sales and Purchase",,
|
||||
Save,حفظ,
|
||||
"Save ${0}?",,
|
||||
@ -761,6 +808,7 @@ Save,حفظ,
|
||||
"Save changes made to ${0}?",,
|
||||
"Save or Submit an entry.",,
|
||||
Saved,,
|
||||
"Search an Item",,
|
||||
"Secured Loans","قروض مضمونة",
|
||||
"Securities and Deposits","الأوراق المالية والودائع",
|
||||
Select,تحديد,
|
||||
@ -777,6 +825,7 @@ Select,تحديد,
|
||||
"Select column",,
|
||||
"Select file","تحديد ملف",
|
||||
"Select folder","تحديد مجلد",
|
||||
"Select the template type.",,
|
||||
Selected,,
|
||||
September,,
|
||||
"Serial Number",,
|
||||
@ -784,6 +833,7 @@ September,,
|
||||
"Serial Number ${0} does not exist.",,
|
||||
"Serial Number ${0} is not Active.",,
|
||||
"Serial Number ${0} is not Inactive",,
|
||||
"Serial Number ${0} status is not Active.",,
|
||||
"Serial Number Description",,
|
||||
"Serial Number is enabled for Item ${0}",,
|
||||
"Serial Number is not enabled for Item ${0}",,
|
||||
@ -793,6 +843,7 @@ Service,الخدمة,
|
||||
"Set Discount Amount",,
|
||||
"Set Period",,
|
||||
"Set Print Size",,
|
||||
"Set Template Type",,
|
||||
"Set Up",,
|
||||
"Set Up Your Workspace","إعداد مساحة العمل الخاصة بك",
|
||||
"Set a Template value to see the Print Template",,
|
||||
@ -813,6 +864,7 @@ Setup,الإعداد,
|
||||
"Setup Wizard","معالج الإعداد",
|
||||
"Setup system defaults like date format and display precision",,
|
||||
Shipment," الشحن ",
|
||||
"Shipment ${0} is Submitted",,
|
||||
"Shipment Item",,
|
||||
"Shipment Location"," موقع شحن البضاعة",
|
||||
"Shipment Number Series"," تسلسل شحن المخزون",
|
||||
@ -865,6 +917,7 @@ Stores," المخازن",
|
||||
Su,,
|
||||
Submit,إرسال,
|
||||
"Submit ${0}?",,
|
||||
"Submit & Print",,
|
||||
"Submit entries?",,
|
||||
Submitted,مقدم,
|
||||
Success,,
|
||||
@ -883,6 +936,8 @@ Tax,الضريبة,
|
||||
"Tax Assets","الأصول الضريبية",
|
||||
"Tax Detail","التفاصيل الضريبية",
|
||||
"Tax ID","المعرّف الضريبي",
|
||||
"Tax Invoice Account",,
|
||||
"Tax Payment Account",,
|
||||
"Tax Rate",,
|
||||
"Tax Summary","ملخص الضريبة",
|
||||
"Tax Template",,
|
||||
@ -891,6 +946,7 @@ Tax,الضريبة,
|
||||
"Taxable Value","القيمة الخاضعة للضريبة",
|
||||
"Taxed Amount",,
|
||||
Taxes,الضرائب,
|
||||
"Taxes and Charges",,
|
||||
Teal,,
|
||||
"Telephone Expenses","مصاريف الهاتف",
|
||||
Template,قالب,
|
||||
@ -943,6 +999,7 @@ Total,اجمالى,
|
||||
"Total Income (Credit)","اجمالى الدخل(دائن)",
|
||||
"Total Liability (Credit)","اجمالى الالتزامات(دائن)",
|
||||
"Total Profit","اجمالى الربح",
|
||||
"Total Quantity",,
|
||||
"Total Spending","إجمالي الإنفاق",
|
||||
"Track Inventory",,
|
||||
Transfer,تحويل,
|
||||
@ -970,7 +1027,6 @@ Unpaid,"غير مدفوعة الأجر",
|
||||
"User Remark","ملاحظة المستخدم",
|
||||
"Utility Expenses","نفقات المرافق",
|
||||
"Validation Error",,
|
||||
"Valuation Method"," طريقة تقييم المخزون",
|
||||
Value,القيمة,
|
||||
"Value missing for ${0}","القيمة مفقودة لـ ${0}",
|
||||
"Value: ${0}",,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user