2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +00:00
This commit is contained in:
Tummas Joensen 2024-04-08 23:20:46 +01:00
commit 4a57f94bb5
114 changed files with 8357 additions and 2871 deletions

View File

@ -59,5 +59,6 @@ module.exports = {
'vite.config.ts',
'postcss.config.js',
'src/components/**/*.vue', // Incrementally fix these
'electron-builder.ts',
],
};

View 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

View 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

View 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
View File

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -23,6 +23,7 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
*.code-workspace
#Electron-builder output
/dist_electron

View File

@ -1,3 +1,5 @@
**/types.ts
**/dist_electron
**/dummy/*.json
**/.github/ISSUE_TEMPLATE/*.yml
**/patches/v0_21_0/*

View File

@ -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 |

View File

@ -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;
}
}

View File

@ -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[];

View 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 };

View File

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

View File

@ -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 */

View File

@ -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,
};

View 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;

View File

@ -11,8 +11,8 @@ mac:
type: distribution
category: public.app-category.finance
icon: build/icon.icns
notarize:
appBundleId: io.frappe.books
# notarize:
# appBundleId: io.frappe.books
hardenedRuntime: true
gatekeeperAssess: false
darkModeSupport: false

View File

@ -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
*/

View File

@ -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;

View File

@ -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);

View File

@ -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,

View File

@ -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',
};

View File

@ -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,34 +303,17 @@ 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;
}
const tax = await this.getTax(item.tax);
for (const { account, rate } of (tax.details ?? []) as TaxDetail[]) {
taxes[account] ??= {
account,
rate,
rate: details.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);
}
}
type Summary = typeof taxes[string] & { idx: number };
const taxArr: Summary[] = [];
@ -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,
};

View File

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

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

@ -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;

View File

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

View File

@ -19,6 +19,8 @@ import { PurchaseInvoice } from './baseModels/PurchaseInvoice/PurchaseInvoice';
import { PurchaseInvoiceItem } from './baseModels/PurchaseInvoiceItem/PurchaseInvoiceItem';
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
import { SalesInvoiceItem } from './baseModels/SalesInvoiceItem/SalesInvoiceItem';
import { SalesQuote } from './baseModels/SalesQuote/SalesQuote';
import { SalesQuoteItem } from './baseModels/SalesQuoteItem/SalesQuoteItem';
import { SetupWizard } from './baseModels/SetupWizard/SetupWizard';
import { Tax } from './baseModels/Tax/Tax';
import { TaxSummary } from './baseModels/TaxSummary/TaxSummary';
@ -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(

View File

@ -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;
},
};
}

View File

@ -0,0 +1,6 @@
import { Doc } from 'fyo/model/doc';
import { Money } from 'pesa';
export abstract class CashDenominations extends Doc {
denomination?: Money;
}

View 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);
},
},
};
}

View File

@ -0,0 +1,5 @@
import { CashDenominations } from './CashDenominations';
export class ClosingCash extends CashDenominations {
count?: number;
}

View File

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

View 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;
}
}

View File

@ -0,0 +1,5 @@
import { CashDenominations } from './CashDenominations';
export class OpeningCash extends CashDenominations {
count?: number;
}

View 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,
}),
};
}

View 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);
}
}

View 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);

View File

@ -27,6 +27,8 @@ export enum ModelNameEnum {
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
SalesInvoice = 'SalesInvoice',
SalesInvoiceItem = 'SalesInvoiceItem',
SalesQuote = 'SalesQuote',
SalesQuoteItem = 'SalesQuoteItem',
SerialNumber = 'SerialNumber',
SetupWizard = 'SetupWizard',
Tax = 'Tax',
@ -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;

View File

@ -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,

View File

@ -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 = [];
for (const rootNode of rootNodes) {
pushToAccountList(rootNode, accountList, 0);
}
return accountList;
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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;

View File

@ -107,6 +107,6 @@ export type Tree = Record<string, TreeNode>;
export type RootTypeRow = {
rootType: AccountRootType;
rootNode: AccountTreeNode;
rootNodes: AccountTreeNode[];
rows: ReportData;
};

View File

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

View File

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

View File

@ -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": [

View File

@ -31,6 +31,14 @@
"target": "Shipment",
"section": "References"
},
{
"fieldname": "quote",
"label": "Quote Reference",
"fieldtype": "Link",
"target": "SalesQuote",
"section": "References",
"required": false
},
{
"fieldname": "makeAutoStockTransfer",
"label": "Make Shipment On Submit",
@ -61,6 +69,12 @@
"target": "SalesInvoice",
"label": "Return Against",
"section": "References"
},
{
"fieldname": "isPOS",
"fieldtype": "Check",
"default": false,
"hidden": true
}
],
"keywordFields": ["name", "party"]

View File

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

View File

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

View File

@ -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"]
}

View File

@ -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",

View File

@ -63,6 +63,13 @@
"fieldtype": "Check",
"default": false,
"section": "Features"
},
{
"fieldname": "enablePointOfSale",
"label": "Enable Point of Sale",
"fieldtype": "Check",
"default": false,
"section": "Features"
}
]
}

View File

@ -0,0 +1,14 @@
{
"name": "CashDenominations",
"label": "Cash Denominations",
"isAbstract": true,
"fields": [
{
"fieldname": "denomination",
"fieldtype": "Currency",
"label": "Denomination",
"placeholder": "Denomination",
"required": true
}
]
}

View 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"
]
}

View 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"]
}

View File

@ -0,0 +1,7 @@
{
"name": "DefaultCashDenominations",
"label": "Default Cash Denominations",
"isChild": true,
"extends": "CashDenominations",
"tableFields": ["denomination"]
}

View 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"]
}

View 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"]
}

View 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"
}
]
}

View 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"
}
]
}

View 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
}
]
}

View 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"
]
}

View File

@ -0,0 +1,4 @@
import { SchemaStub } from '../../types';
import AccountingSettings from './AccountingSettings.json';
export default [AccountingSettings] as SchemaStub[];

View File

@ -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[]
>;

View File

@ -25,6 +25,8 @@ import PurchaseInvoice from './app/PurchaseInvoice.json';
import PurchaseInvoiceItem from './app/PurchaseInvoiceItem.json';
import SalesInvoice from './app/SalesInvoice.json';
import SalesInvoiceItem from './app/SalesInvoiceItem.json';
import SalesQuote from './app/SalesQuote.json';
import SalesQuoteItem from './app/SalesQuoteItem.json';
import SetupWizard from './app/SetupWizard.json';
import Tax from './app/Tax.json';
import TaxDetail from './app/TaxDetail.json';
@ -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,
];

View File

@ -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() {

View File

@ -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() {

View File

@ -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

View File

@ -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;
},

View File

@ -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,

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
}

View File

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

View File

@ -0,0 +1,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>

View 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
View 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>

View 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>

View File

@ -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[]> {

View File

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

View File

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

View File

@ -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() });

View File

@ -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
View 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);
}
}

View File

@ -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) => {
return [
ModelNameEnum.SalesInvoice,
ModelNameEnum.SalesQuote,
ModelNameEnum.PurchaseInvoice,
].map((schemaName) => {
const label = fyo.schemaMap[schemaName]?.label ?? schemaName;
return { name: `${name} - ${label}`, type: schemaName };
}
);
});
}
export const baseTemplate = `<main class="h-full w-full bg-white">

View File

@ -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`,

View File

@ -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}",,

1 ${0} ${1} already exists. ${0} ${0} ${1} موجودة من قبل.
1 ${0}
2 ${0} ${1} already exists. ${0} ${1} already exists. ${0} ${1} موجودة من قبل. ${0} ${1} موجودة من قبل.
3 ${0} ${1} does not exist ${0} ${1} does not exist ${0} ${1} غير موجود
4 ${0} ${1} has been modified after loading please reload entry. ${0} ${1} has been modified after loading please reload entry. ${0} ${1} تم تعديله بعد التحميل يرجى إعادة تحميل الإدخال.
5 ${0} ${1} is linked with existing records. ${0} ${1} is linked with existing records. ${0} ${1} مرطبت بسجل موجود. ${0} ${1} مرطبت بسجل موجود.
6 ${0} account not set in Inventory Settings. ${0} account not set in Inventory Settings. ${0} لم يتم تعيين الحساب في إعدادات المخزون.
7 ${0} already saved ${0} already saved ${0} تم حفظ بالفعل
8 ${0} already submitted ${0} already submitted ${0} تم الارسال بالفعل
9 ${0} cancelled ${0} cancelled ${0} تم الالغاء
10 ${0} cannot be cancelled ${0} cannot be cancelled ${0} لا يمكن الالغاء
11 ${0} cannot be deleted ${0} cannot be deleted ${0} لا يمكن الحذف
12 ${0} deleted ${0} deleted ${0} تم الحذف
13 ${0} entries failed ${0} entries failed ${0} فشل الادخال
14 ${0} entries imported ${0} entries imported ${0} الإدخالات المستوردة
15 ${0} entry failed ${0} entry failed ${0} فشل الإدخال
16 ${0} entry imported ${0} entry imported ${0} الإدخال مستورد
17 ${0} fields selected ${0} fields selected ${0} الحقول المحددة
18 ${0} filters applied ${0} filters applied ${0} تم تطبيق الفيلتر ${0} تم تطبيق الفيلتر
19 ${0} has linked child accounts. ${0} has linked child accounts. ${0} لديه حسابات فرعية مرتبطة.
20 ${0} of type ${1} does not exist ${0} of type ${1} does not exist ${0} من النوع ${1} غير موجود
21 ${0} out of ${1} ${0} out of ${1}
22 ${0} party ${1} is different from ${2} ${0} party ${1} is different from ${2}
23 ${0} quantity 1 added. ${0} quantity 1 added.
71 Add products or services that you buy from your suppliers Add products or services that you buy from your suppliers
72 Add products or services that you sell to your customers Add products or services that you sell to your customers
73 Add transfer terms Add transfer terms
74 Add'l Discounts
75 Additional ${0} Serial Numbers required for ${1} quantity of ${2}. Additional ${0} Serial Numbers required for ${1} quantity of ${2}.
76 Additional quantity (${0}) required${1} to make outward transfer of item ${2} from ${3} on ${4} Additional quantity (${0}) required${1} to make outward transfer of item ${2} from ${3} on ${4}
77 Address Address العنوان العنوان
107 Auto Payments Auto Payments خاصية الدفع التلقائي خاصية الدفع التلقائي
108 Auto Stock Transfer Auto Stock Transfer خاصية نقل المنتجات تلقائيًا بين المخازن خاصية نقل المنتجات تلقائيًا بين المخازن
109 Autocomplete Autocomplete
110 Back
111 Back Reference Back Reference
112 Bad import data, could not read file. Bad import data, could not read file.
113 Balance Balance التوازن التوازن
114 Balance Amount
115 Balance Sheet Balance Sheet تقرير الميزانية تقرير الميزانية
116 Bank Bank
117 Bank Accounts Bank Accounts حسابات بنكية حسابات بنكية
142 Cannot Import Cannot Import
143 Cannot Open File Cannot Open File
144 Cannot cancel ${0} ${1} because of the following ${2}: ${3} Cannot cancel ${0} ${1} because of the following ${2}: ${3}
145 Cannot cancel ${0} because of the following ${1}: ${2}
146 Cannot delete ${0} "${1}" because of linked entries. Cannot delete ${0} "${1}" because of linked entries.
147 Cannot open file Cannot open file
148 Cannot perform operation. Cannot perform operation.
150 Capital Equipments Capital Equipments معدات رأس المال معدات رأس المال
151 Capital Stock Capital Stock رأس المال رأس المال
152 Cash Cash نقد نقد
153 Cash Denominations
154 Cash Entry Cash Entry قيود النقدية قيود النقدية
155 Cash In Hand Cash In Hand نقدا في اليد نقدا في اليد
156 Cashflow Cashflow التدفق النقدي التدفق النقدي
169 Clearance Date Clearance Date تاريخ التخليص تاريخ التخليص
170 Close Close إغلاق إغلاق
171 Close Frappe Books and try manually. Close Frappe Books and try manually.
172 Close POS Shift
173 Close Quick Search Close Quick Search
174 Closing Closing اغلاق اغلاق
175 Closing ${0} Amount can not be negative.
176 Closing (Cr) Closing (Cr) (Cr) إغلاق (Cr) إغلاق
177 Closing (Dr) Closing (Dr) (Dr) إغلاق (Dr) إغلاق
178 Closing Amount
179 Closing Cash In Denominations
180 Closing Date
181 Collapse Collapse
182 Color Color اللون اللون
183 Commission on Sales Commission on Sales عمولة على المبيعات عمولة على المبيعات
198 Cost Of Goods Sold Acc. Cost Of Goods Sold Acc. حساب تكلفة البضاعة المباعة حساب تكلفة البضاعة المباعة
199 Cost of Goods Sold Cost of Goods Sold تكلفة البضائع المباعة تكلفة البضائع المباعة
200 Could not connect to database file ${0}, please select the file manually Could not connect to database file ${0}, please select the file manually
201 Count
202 Counter Cash Account
203 Country Country البلد البلد
204 Country Code Country Code كود البلد كود البلد
205 Country code used to initialize regional settings. Country code used to initialize regional settings. كود البلد يستخدم لتثبت اعدادات الاقليمية كود البلد يستخدم لتثبت اعدادات الاقليمية
259 Decrease print template display scale Decrease print template display scale
260 Default Default
261 Default Account Default Account الحساب الافتراضي الحساب الافتراضي
262 Default Cash Denominations
263 Default Location Default Location الموقع الافتراضي الموقع الافتراضي
264 Defaults Defaults الاعدادات الافتراضية الاعدادات الافتراضية
265 Delete Delete حذف حذف
268 Delete Failed Delete Failed
269 Delete Group Delete Group
270 Delivered Delivered
271 Denomination
272 Depreciation Depreciation إهلاك إهلاك
273 Depreciation Entry Depreciation Entry ادخال قيد الاهلاك ادخال قيد الاهلاك
274 Description Description الوصف الوصف
275 Details Details التفاصيل التفاصيل
276 Difference Amount
277 Direct Expenses Direct Expenses المصاريف المباشرة المصاريف المباشرة
278 Direct Income Direct Income الدخل المباشر الدخل المباشر
279 Directory for database file ${0} does not exist, please select the file manually Directory for database file ${0} does not exist, please select the file manually
317 Enable Discount Accounting Enable Discount Accounting تفعيل حساب الخصم تفعيل حساب الخصم
318 Enable Form Customization Enable Form Customization
319 Enable Inventory Enable Inventory تفعيل المخزون تفعيل المخزون
320 Enable Invoice Returns
321 Enable Point of Sale
322 Enable Price List Enable Price List تفعيل قائمة الاسعار تفعيل قائمة الاسعار
323 Enable Serial Number Enable Serial Number تفعيل السريال نمبر تفعيل السريال نمبر
324 Enable Stock Returns Enable Stock Returns
343 Excise Entry Excise Entry
344 Existing Company Existing Company
345 Expand Expand
346 Expected Amount
347 Expense Expense المصاريف المصاريف
348 Expense Account Expense Account حساب المصروف حساب المصروف
349 Expenses Expenses المصروفات المصروفات
354 Export Format Export Format
355 Export Successful Export Successful تم التصدير بنجاح تم التصدير بنجاح
356 Export Wizard Export Wizard
FIFO
357 Failed Failed
358 Fax Fax فاكس فاكس
359 Features Features الخصائص الخصائص
445 Instance Id Instance Id
446 Insufficient Quantity Insufficient Quantity
447 Insufficient Quantity. Insufficient Quantity.
448 Insufficient Quantity. Item ${0} has only ${1} quantities available. you selected ${2}
449 Int Int
450 Intergrated Tax Intergrated Tax ضريبة متكاملة ضريبة متكاملة
451 Internal Precision Internal Precision الدقة الداخلية الدقة الداخلية
452 Invalid Key Error Invalid Key Error
453 Invalid Quantity for Item ${0}
454 Invalid barcode value ${0}. Invalid barcode value ${0}.
455 Invalid value ${0} for ${1} Invalid value ${0} for ${1}
456 Invalid value found for ${0} Invalid value found for ${0}
472 Is Landscape Is Landscape
473 Is Not Is Not ليس ليس
474 Is Not Empty Is Not Empty ليس فارغ ليس فارغ
475 Is POS Shift Open
476 Is Price List Enabled Is Price List Enabled
477 Is Required Is Required
478 Is Whole Is Whole
479 Item Item صنف صنف
480 Item ${0} has Zero Quantity
481 Item ${0} is a batched item Item ${0} is a batched item
482 Item ${0} is not a batched item Item ${0} is not a batched item
483 Item ${0} not in Stock
484 Item Description Item Description
485 Item Discounts
486 Item Name Item Name اسم العنصر اسم العنصر
487 Item Prices Item Prices
488 Item with From location not found Item with From location not found
551 More Filters More Filters
552 More shortcuts will be added soon. More shortcuts will be added soon.
553 Movement Type Movement Type نوع التنقل نوع التنقل
Moving Average
554 Name Name الاسم الاسم
555 Navigate Navigate
556 Need ${0} Serial Numbers for Item ${1}. You have provided ${2}
557 Net Total Net Total الإجمالي الصافي الإجمالي الصافي
558 New ${0} New ${0}
559 New ${0} ${1} New ${0} ${1}
606 Open the Export Wizard modal Open the Export Wizard modal
607 Opening (Cr) Opening (Cr) (Cr) الافتتاح (Cr) الافتتاح
608 Opening (Dr) Opening (Dr) (Dr) افتتاح (Dr) افتتاح
609 Opening Amount
610 Opening Balance Equity Opening Balance Equity الرصيد الافتتاحي حقوق الملكية الرصيد الافتتاحي حقوق الملكية
611 Opening Balances Opening Balances أرصدة الافتتاح أرصدة الافتتاح
612 Opening Cash Amount can not be negative.
613 Opening Cash In Denominations
614 Opening Date
615 Opening Entry Opening Entry
616 Options Options
617 Orange Orange
619 Outflow Outflow التدفق التدفق
620 Outstanding Outstanding
621 Outstanding Amount Outstanding Amount المبلغ المستحق المبلغ المستحق
622 POS
623 POS Counter Cash Account is not set. Please set it on POS Settings
624 POS Customer
625 POS Inventory is not set. Please set it on POS Settings
626 POS Settings
627 POS Shift Amount
628 POS Write Off Account is not set. Please set it on POS Settings
629 Pad Zeros Pad Zeros
630 Page Page
631 Paid Paid مدفوع مدفوع
632 Paid ${0} Paid ${0}
633 Paid Change
634 Parent Parent الوالد الوالد
635 Parent Account Parent Account الحساب الوالد الحساب الوالد
636 Party Party مورد / عميل مورد / عميل
638 Pay Pay دفع دفع
639 Payable Payable
640 Payment Payment الدفع الدفع
641 Payment ${0} is Saved
642 Payment For Payment For الدفع مقابل الدفع مقابل
643 Payment Method Payment Method طريقة الدفع طريقة الدفع
644 Payment No Payment No رقم الدفع رقم الدفع
674 Please set GSTIN in General Settings. Please set GSTIN in General Settings.
675 Please set Round Off Account in the Settings. Please set Round Off Account in the Settings.
676 Please set a Display Doc Please set a Display Doc
677 Point of Sale
678 Postal Code Postal Code الرمز البريدي الرمز البريدي
679 Postal Expenses Postal Expenses المصاريف البريدية المصاريف البريدية
680 Posting Date Posting Date تاريخ النشر تاريخ النشر
718 Purchases Purchases المشتريات المشتريات
719 Purple Purple
720 Purpose Purpose
721 Qty in Batch
722 Qty. ${0} Qty. ${0}
723 Qty. in Transfer Unit Qty. in Transfer Unit
724 Quantity Quantity الكمية الكمية
728 Quarters Quarters
729 Quick Search Quick Search
730 Quick edit error: ${0} entry has no name. Quick edit error: ${0} entry has no name.
731 Quote
732 Quote Reference
733 Rate Rate معدل معدل
734 Rate (${0}) cannot be less zero. Rate (${0}) cannot be less zero. لا يمكن أن يكون السعر (${0}) أقل من صفر. لا يمكن أن يكون السعر (${0}) أقل من صفر.
735 Rate (${0}) has to be greater than zero Rate (${0}) has to be greater than zero
780 Sales Acc. Sales Acc. حساب المبيعات حساب المبيعات
781 Sales Expenses Sales Expenses مصاريف المبيعات مصاريف المبيعات
782 Sales Invoice Sales Invoice فاتورة مبيعات فاتورة مبيعات
783 Sales Invoice ${0} is Submitted
784 Sales Invoice Item Sales Invoice Item اصناف فاتورة المبيعات اصناف فاتورة المبيعات
785 Sales Invoice Number Series Sales Invoice Number Series تسلسل فاتورة المبيعات تسلسل فاتورة المبيعات
786 Sales Invoice Print Template Sales Invoice Print Template قالب الطابعة فاتورة المبيعات قالب الطابعة فاتورة المبيعات
792 Sales Payment Sales Payment
793 Sales Payment Account Sales Payment Account حساب دفع المبيعات حساب دفع المبيعات
794 Sales Payments Sales Payments مدفوعات المبيعات مدفوعات المبيعات
795 Sales Quote
796 Sales Quote Item
797 Sales Quote Number Series
798 Sales Quote Print Template
799 Sales Quotes
800 Sales and Purchase Sales and Purchase
801 Save Save حفظ حفظ
802 Save ${0}? Save ${0}?
808 Save changes made to ${0}? Save changes made to ${0}?
809 Save or Submit an entry. Save or Submit an entry.
810 Saved Saved
811 Search an Item
812 Secured Loans Secured Loans قروض مضمونة قروض مضمونة
813 Securities and Deposits Securities and Deposits الأوراق المالية والودائع الأوراق المالية والودائع
814 Select Select تحديد تحديد
825 Select column Select column
826 Select file Select file تحديد ملف تحديد ملف
827 Select folder Select folder تحديد مجلد تحديد مجلد
828 Select the template type.
829 Selected Selected
830 September September
831 Serial Number Serial Number
833 Serial Number ${0} does not exist. Serial Number ${0} does not exist.
834 Serial Number ${0} is not Active. Serial Number ${0} is not Active.
835 Serial Number ${0} is not Inactive Serial Number ${0} is not Inactive
836 Serial Number ${0} status is not Active.
837 Serial Number Description Serial Number Description
838 Serial Number is enabled for Item ${0} Serial Number is enabled for Item ${0}
839 Serial Number is not enabled for Item ${0} Serial Number is not enabled for Item ${0}
843 Set Discount Amount Set Discount Amount
844 Set Period Set Period
845 Set Print Size Set Print Size
846 Set Template Type
847 Set Up Set Up
848 Set Up Your Workspace Set Up Your Workspace إعداد مساحة العمل الخاصة بك إعداد مساحة العمل الخاصة بك
849 Set a Template value to see the Print Template Set a Template value to see the Print Template
864 Setup Wizard Setup Wizard معالج الإعداد معالج الإعداد
865 Setup system defaults like date format and display precision Setup system defaults like date format and display precision
866 Shipment Shipment الشحن الشحن
867 Shipment ${0} is Submitted
868 Shipment Item Shipment Item
869 Shipment Location Shipment Location موقع شحن البضاعة موقع شحن البضاعة
870 Shipment Number Series Shipment Number Series تسلسل شحن المخزون تسلسل شحن المخزون
917 Su Su
918 Submit Submit إرسال إرسال
919 Submit ${0}? Submit ${0}?
920 Submit & Print
921 Submit entries? Submit entries?
922 Submitted Submitted مقدم مقدم
923 Success Success
936 Tax Assets Tax Assets الأصول الضريبية الأصول الضريبية
937 Tax Detail Tax Detail التفاصيل الضريبية التفاصيل الضريبية
938 Tax ID Tax ID المعرّف الضريبي المعرّف الضريبي
939 Tax Invoice Account
940 Tax Payment Account
941 Tax Rate Tax Rate
942 Tax Summary Tax Summary ملخص الضريبة ملخص الضريبة
943 Tax Template Tax Template
946 Taxable Value Taxable Value القيمة الخاضعة للضريبة القيمة الخاضعة للضريبة
947 Taxed Amount Taxed Amount
948 Taxes Taxes الضرائب الضرائب
949 Taxes and Charges
950 Teal Teal
951 Telephone Expenses Telephone Expenses مصاريف الهاتف مصاريف الهاتف
952 Template Template قالب قالب
999 Total Income (Credit) Total Income (Credit) اجمالى الدخل(دائن) اجمالى الدخل(دائن)
1000 Total Liability (Credit) Total Liability (Credit) اجمالى الالتزامات(دائن) اجمالى الالتزامات(دائن)
1001 Total Profit Total Profit اجمالى الربح اجمالى الربح
1002 Total Quantity
1003 Total Spending Total Spending إجمالي الإنفاق إجمالي الإنفاق
1004 Track Inventory Track Inventory
1005 Transfer Transfer تحويل تحويل
1027 User Remark User Remark ملاحظة المستخدم ملاحظة المستخدم
1028 Utility Expenses Utility Expenses نفقات المرافق نفقات المرافق
1029 Validation Error Validation Error
Valuation Method طريقة تقييم المخزون
1030 Value Value القيمة القيمة
1031 Value missing for ${0} Value missing for ${0} القيمة مفقودة لـ ${0} القيمة مفقودة لـ ${0}
1032 Value: ${0} Value: ${0}

Some files were not shown because too many files have changed in this diff Show More