mirror of
https://github.com/frappe/books.git
synced 2024-11-09 23:30:56 +00:00
Merge branch 'master' of https://github.com/frappe/books into batch-wise-item
This commit is contained in:
commit
0ac24bfe77
26
README.md
26
README.md
@ -128,19 +128,19 @@ If you want to contribute code then you can fork this repo, make changes and rai
|
||||
|
||||
## Translation Contributors
|
||||
|
||||
| Language | Contributors |
|
||||
| ------------------ | ---------------------------------------------------------------------------------- |
|
||||
| French | [DeepL](https://www.deepl.com/), [mael-chouteau](https://github.com/mael-chouteau) |
|
||||
| German | [DeepL](https://www.deepl.com/), [barredterra](https://github.com/barredterra) |
|
||||
| Portuguese | [DeepL](https://www.deepl.com/) |
|
||||
| Arabic | [taha2002](https://github.com/taha2002) |
|
||||
| Catalan | Dídac E. Jiménez |
|
||||
| Dutch | [FastAct](https://github.com/FastAct) |
|
||||
| Spanish | [talmax1124](https://github.com/talmax1124) |
|
||||
| Gujarati | [dhruvilxcode](https://github.com/dhruvilxcode) |
|
||||
| Korean | [Isaac-Kwon](https://github.com/Isaac-Kwon) |
|
||||
| Simplified Chinese | [wcxu21](https://github.com/wcxu21) |
|
||||
| Swedish | [papplo](https://github.com/papplo) |
|
||||
| Language | Contributors |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| French | [DeepL](https://www.deepl.com/), [mael-chouteau](https://github.com/mael-chouteau) |
|
||||
| German | [DeepL](https://www.deepl.com/), [barredterra](https://github.com/barredterra) |
|
||||
| Portuguese | [DeepL](https://www.deepl.com/) |
|
||||
| Arabic | [taha2002](https://github.com/taha2002) |
|
||||
| Catalan | Dídac E. Jiménez |
|
||||
| Dutch | [FastAct](https://github.com/FastAct) |
|
||||
| Spanish | [talmax1124](https://github.com/talmax1124) |
|
||||
| Gujarati | [dhruvilxcode](https://github.com/dhruvilxcode), [4silvertooth](https://github.com/4silvertooth) |
|
||||
| Korean | [Isaac-Kwon](https://github.com/Isaac-Kwon) |
|
||||
| Simplified Chinese | [wcxu21](https://github.com/wcxu21) |
|
||||
| Swedish | [papplo](https://github.com/papplo) |
|
||||
|
||||
## License
|
||||
|
||||
|
@ -295,7 +295,7 @@ function toDocAttachment(value: RawValue, field: Field): null | Attachment {
|
||||
|
||||
function toRawCurrency(value: DocValue, fyo: Fyo, field: Field): string {
|
||||
if (isPesa(value)) {
|
||||
return (value as Money).store;
|
||||
return value.store;
|
||||
}
|
||||
|
||||
if (getIsNullOrUndef(value)) {
|
||||
|
@ -157,6 +157,22 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return false;
|
||||
}
|
||||
|
||||
get canEdit() {
|
||||
if (!this.schema.isSubmittable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.submitted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cancelled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get canSave() {
|
||||
if (!!this.submitted) {
|
||||
return false;
|
||||
@ -543,7 +559,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
if (isPesa(value)) {
|
||||
value = (value as Money).copy();
|
||||
value = value.copy();
|
||||
}
|
||||
|
||||
if (value === null && this.schema.isSingle) {
|
||||
@ -966,7 +982,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return value as Money;
|
||||
|
||||
return value;
|
||||
})
|
||||
.reduce((a, b) => a.add(b), this.fyo.pesa(0));
|
||||
|
||||
|
@ -17,7 +17,7 @@ export function areDocValuesEqual(
|
||||
|
||||
if (isPesa(dvOne)) {
|
||||
try {
|
||||
return (dvOne as Money).eq(dvTwo as string | number);
|
||||
return dvOne.eq(dvTwo as string | number);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@ -134,7 +134,7 @@ function shouldApplyFormulaPreSync(
|
||||
|
||||
export function isDocValueTruthy(docValue: DocValue | Doc[]) {
|
||||
if (isPesa(docValue)) {
|
||||
return !(docValue as Money).isZero();
|
||||
return !docValue.isZero();
|
||||
}
|
||||
|
||||
if (Array.isArray(docValue)) {
|
||||
|
@ -36,7 +36,7 @@ export function getDuplicates(array: unknown[]) {
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
export function isPesa(value: unknown): boolean {
|
||||
export function isPesa(value: unknown): value is Money {
|
||||
return value instanceof Money;
|
||||
}
|
||||
|
||||
|
@ -6,13 +6,11 @@ import {
|
||||
DefaultMap,
|
||||
FiltersMap,
|
||||
FormulaMap,
|
||||
HiddenMap
|
||||
HiddenMap,
|
||||
} from 'fyo/model/types';
|
||||
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import {
|
||||
getExchangeRate, getNumberSeries
|
||||
} from 'models/helpers';
|
||||
import { addItem, getExchangeRate, getNumberSeries } from 'models/helpers';
|
||||
import { InventorySettings } from 'models/inventory/InventorySettings';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
import { Transactional } from 'models/Transactional/Transactional';
|
||||
@ -695,4 +693,8 @@ export abstract class Invoice extends Transactional {
|
||||
}))
|
||||
.sort((a, b) => a.date.valueOf() - b.date.valueOf());
|
||||
}
|
||||
|
||||
async addItem(name: string) {
|
||||
return await addItem(name, this);
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,7 @@ export class Item extends Doc {
|
||||
this.itemType !== 'Product' ||
|
||||
(this.inserted && !this.trackItem),
|
||||
hasBatchNumber: () => !this.trackItem || false,
|
||||
barcode: () => !this.fyo.singles.InventorySettings?.enableBarcodes,
|
||||
};
|
||||
|
||||
readOnly: ReadOnlyMap = {
|
||||
|
@ -14,6 +14,8 @@ import {
|
||||
numberSeriesDefaultsMap,
|
||||
} from './baseModels/Defaults/Defaults';
|
||||
import { Invoice } from './baseModels/Invoice/Invoice';
|
||||
import { StockMovement } from './inventory/StockMovement';
|
||||
import { StockTransfer } from './inventory/StockTransfer';
|
||||
import { InvoiceStatus, ModelNameEnum } from './types';
|
||||
|
||||
export function getInvoiceActions(
|
||||
@ -321,3 +323,27 @@ export function getDocStatusListColumn(): ColumnConfig {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type ModelsWithItems = Invoice | StockTransfer | StockMovement;
|
||||
export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
|
||||
if (!doc.canEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = (doc.items ?? []) as NonNullable<M['items']>[number][];
|
||||
|
||||
let item = items.find((i) => i.item === name);
|
||||
if (item) {
|
||||
const q = item.quantity ?? 0;
|
||||
await item.set('quantity', q + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
await doc.append('items');
|
||||
item = doc.items?.at(-1);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
await item.set('item', name);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ export class InventorySettings extends Doc {
|
||||
valuationMethod?: ValuationMethod;
|
||||
stockReceivedButNotBilled?: string;
|
||||
costOfGoodsSold?: string;
|
||||
enableBarcodes?: boolean;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
stockInHand: () => ({
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
FormulaMap,
|
||||
ListViewSettings,
|
||||
} from 'fyo/model/types';
|
||||
import { getDocStatusListColumn, getLedgerLinkAction } from 'models/helpers';
|
||||
import { addItem, getDocStatusListColumn, getLedgerLinkAction } from 'models/helpers';
|
||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
@ -93,4 +93,8 @@ export class StockMovement extends Transfer {
|
||||
static getActions(fyo: Fyo): Action[] {
|
||||
return [getLedgerLinkAction(fyo, true)];
|
||||
}
|
||||
|
||||
async addItem(name: string) {
|
||||
return await addItem(name, this);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { Action, DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { Defaults } from 'models/baseModels/Defaults/Defaults';
|
||||
import { Invoice } from 'models/baseModels/Invoice/Invoice';
|
||||
import { getLedgerLinkAction, getNumberSeries } from 'models/helpers';
|
||||
import { addItem, getLedgerLinkAction, getNumberSeries } from 'models/helpers';
|
||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
@ -233,4 +233,8 @@ export abstract class StockTransfer extends Transfer {
|
||||
role: doc.isSales ? 'Customer' : 'Supplier',
|
||||
}),
|
||||
};
|
||||
|
||||
async addItem(name: string) {
|
||||
return await addItem(name, this);
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,12 @@
|
||||
"fieldtype": "Int",
|
||||
"placeholder": "HSN/SAC Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "barcode",
|
||||
"label": "Barcode",
|
||||
"fieldtype": "Data",
|
||||
"placeholder": "Barcode"
|
||||
},
|
||||
{
|
||||
"fieldname": "trackItem",
|
||||
"label": "Track Item",
|
||||
@ -128,6 +134,7 @@
|
||||
"description",
|
||||
"incomeAccount",
|
||||
"expenseAccount",
|
||||
"barcode",
|
||||
"hsnCode",
|
||||
"trackItem",
|
||||
"hasBatchNumber"
|
||||
|
@ -45,6 +45,11 @@
|
||||
"label": "Cost Of Goods Sold Acc.",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "enableBarcodes",
|
||||
"label": "Enable Barcodes",
|
||||
"fieldtype": "Check"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
130
src/components/Controls/Barcode.vue
Normal file
130
src/components/Controls/Barcode.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
border
|
||||
w-36
|
||||
rounded
|
||||
px-2
|
||||
bg-gray-50
|
||||
focus-within:bg-gray-100
|
||||
"
|
||||
>
|
||||
<input
|
||||
ref="scanner"
|
||||
type="text"
|
||||
class="text-base placeholder-gray-600 w-full bg-transparent outline-none"
|
||||
@change="handleChange"
|
||||
:placeholder="t`Enter barcode`"
|
||||
/>
|
||||
<feather-icon
|
||||
name="maximize"
|
||||
class="w-3 h-3 text-gray-600 cursor-text"
|
||||
@click="() => ($refs.scanner as HTMLInputElement).focus()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { showToast } from 'src/utils/ui';
|
||||
import { defineComponent } from 'vue';
|
||||
export default defineComponent({
|
||||
emits: ['item-selected'],
|
||||
data() {
|
||||
return {
|
||||
timerId: null,
|
||||
barcode: '',
|
||||
} as {
|
||||
timerId: null | ReturnType<typeof setInterval>;
|
||||
barcode: string;
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.scanListener);
|
||||
},
|
||||
unmounted() {
|
||||
document.removeEventListener('keydown', this.scanListener);
|
||||
},
|
||||
activated() {
|
||||
document.addEventListener('keydown', this.scanListener);
|
||||
},
|
||||
deactivated() {
|
||||
document.removeEventListener('keydown', this.scanListener);
|
||||
},
|
||||
methods: {
|
||||
handleChange(e: Event) {
|
||||
const elem = e.target as HTMLInputElement;
|
||||
this.selectItem(elem.value);
|
||||
elem.value = '';
|
||||
},
|
||||
async selectItem(code: string) {
|
||||
const barcode = code.trim();
|
||||
if (!/\d{12,}/.test(barcode)) {
|
||||
return this.error(this.t`Invalid barcode value ${barcode}.`);
|
||||
}
|
||||
|
||||
const items = (await this.fyo.db.getAll('Item', {
|
||||
filters: { barcode },
|
||||
fields: ['name'],
|
||||
})) as { name: string }[];
|
||||
|
||||
const name = items?.[0]?.name;
|
||||
if (!name) {
|
||||
return this.error(this.t`Item with barcode ${barcode} not found.`);
|
||||
}
|
||||
|
||||
this.success(this.t`${name} quantity 1 added.`);
|
||||
this.$emit('item-selected', name);
|
||||
},
|
||||
async scanListener({ key, code }: KeyboardEvent) {
|
||||
/**
|
||||
* Based under the assumption that
|
||||
* - Barcode scanners trigger keydown events
|
||||
* - Keydown events are triggered quicker than human can
|
||||
* i.e. at max 20ms between events
|
||||
* - Keydown events are triggered for barcode digits
|
||||
* - The sequence of digits might be punctuated by a return
|
||||
*/
|
||||
|
||||
const keyCode = Number(key);
|
||||
const isEnter = code === 'Enter';
|
||||
if (Number.isNaN(keyCode) && !isEnter) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEnter) {
|
||||
return await this.setItemFromBarcode();
|
||||
}
|
||||
|
||||
if (this.timerId !== null) {
|
||||
clearInterval(this.timerId);
|
||||
}
|
||||
|
||||
this.barcode += key;
|
||||
this.timerId = setInterval(async () => {
|
||||
await this.setItemFromBarcode();
|
||||
this.barcode = '';
|
||||
}, 20);
|
||||
},
|
||||
async setItemFromBarcode() {
|
||||
if (this.barcode.length < 12) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.selectItem(this.barcode);
|
||||
|
||||
this.barcode = '';
|
||||
if (this.timerId !== null) {
|
||||
clearInterval(this.timerId);
|
||||
}
|
||||
},
|
||||
error(message: string) {
|
||||
showToast({ type: 'error', message });
|
||||
},
|
||||
success(message: string) {
|
||||
showToast({ type: 'success', message });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-center bg-gray-100 rounded-md textsm px-1">
|
||||
<div class="flex items-center bg-gray-50 rounded-md textsm px-1 border">
|
||||
<div
|
||||
class="rate-container"
|
||||
:class="disabled ? 'bg-gray-100' : 'bg-gray-25'"
|
||||
@ -89,12 +89,11 @@ export default defineComponent({
|
||||
</script>
|
||||
<style scoped>
|
||||
input[type='number'] {
|
||||
@apply w-12 outline-none bg-transparent p-0.5;
|
||||
@apply w-12 bg-transparent p-0.5;
|
||||
}
|
||||
|
||||
.rate-container {
|
||||
@apply flex items-center rounded-md border border-gray-100 text-gray-900
|
||||
text-sm outline-none focus-within:bg-gray-50 px-1 focus-within:border-gray-200;
|
||||
@apply flex items-center rounded-md border-gray-100 text-gray-900 text-sm px-1 focus-within:border-gray-200 bg-transparent;
|
||||
}
|
||||
|
||||
.rate-container > p {
|
||||
|
@ -21,7 +21,6 @@
|
||||
bg-white
|
||||
rounded-lg
|
||||
shadow-2xl
|
||||
w-form
|
||||
border
|
||||
overflow-hidden
|
||||
inner
|
||||
@ -44,10 +43,6 @@ export default defineComponent({
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
setCloseListener: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
emits: ['closemodal'],
|
||||
watch: {
|
||||
|
@ -17,7 +17,7 @@
|
||||
:set-close-listener="false"
|
||||
>
|
||||
<!-- Search Input -->
|
||||
<div class="p-1">
|
||||
<div class="p-1 w-form">
|
||||
<input
|
||||
ref="input"
|
||||
type="search"
|
||||
|
@ -167,7 +167,7 @@
|
||||
|
||||
<!-- Base Count Selection when Dev -->
|
||||
<Modal :open-modal="openModal" @closemodal="openModal = false">
|
||||
<div class="p-4 text-gray-900">
|
||||
<div class="p-4 text-gray-900 w-form">
|
||||
<h2 class="text-xl font-semibold select-none">Set Base Count</h2>
|
||||
<p class="text-base mt-2">
|
||||
Base Count is a lower bound on the number of entries made when
|
||||
|
@ -3,6 +3,10 @@
|
||||
<!-- Page Header (Title, Buttons, etc) -->
|
||||
<template #header v-if="doc">
|
||||
<StatusBadge :status="status" />
|
||||
<Barcode
|
||||
v-if="showBarcode"
|
||||
@item-selected="(name) => doc.addItem(name)"
|
||||
/>
|
||||
<DropdownWithActions
|
||||
v-for="group of groupedActions"
|
||||
:key="group.label"
|
||||
@ -145,7 +149,9 @@
|
||||
import { computed } from '@vue/reactivity';
|
||||
import { t } from 'fyo';
|
||||
import { getDocStatus } from 'models/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Barcode from 'src/components/Controls/Barcode.vue';
|
||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||
import Table from 'src/components/Controls/Table.vue';
|
||||
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
||||
@ -176,6 +182,7 @@ export default {
|
||||
FormContainer,
|
||||
QuickEditForm,
|
||||
FormHeader,
|
||||
Barcode,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@ -204,6 +211,25 @@ export default {
|
||||
groupedActions() {
|
||||
return getGroupedActionsForDoc(this.doc);
|
||||
},
|
||||
showBarcode() {
|
||||
if (!this.doc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.doc.canEdit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fyo.singles.InventorySettings?.enableBarcodes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
ModelNameEnum.Shipment,
|
||||
ModelNameEnum.PurchaseReceipt,
|
||||
ModelNameEnum.StockMovement,
|
||||
].includes(this.schemaName);
|
||||
},
|
||||
},
|
||||
activated() {
|
||||
docsPath.value = docsPathMap[this.schemaName];
|
||||
|
@ -13,6 +13,10 @@
|
||||
async (exchangeRate) => await doc.set('exchangeRate', exchangeRate)
|
||||
"
|
||||
/>
|
||||
<Barcode
|
||||
v-if="doc.canEdit && fyo.singles.InventorySettings?.enableBarcodes"
|
||||
@item-selected="(name) => doc.addItem(name)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!doc.isCancelled && !doc.dirty"
|
||||
:icon="true"
|
||||
@ -298,6 +302,7 @@ import { computed } from '@vue/reactivity';
|
||||
import { getDocStatus } from 'models/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import Button from 'src/components/Button.vue';
|
||||
import Barcode from 'src/components/Controls/Barcode.vue';
|
||||
import ExchangeRate from 'src/components/Controls/ExchangeRate.vue';
|
||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||
import Table from 'src/components/Controls/Table.vue';
|
||||
@ -332,6 +337,7 @@ export default {
|
||||
ExchangeRate,
|
||||
FormHeader,
|
||||
LinkedEntryWidget,
|
||||
Barcode,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
|
@ -31,6 +31,7 @@
|
||||
/>
|
||||
<Modal :open-modal="openExportModal" @closemodal="openExportModal = false">
|
||||
<ExportWizard
|
||||
class="w-form"
|
||||
:schema-name="schemaName"
|
||||
:title="pageTitle"
|
||||
:list-filters="listFilters"
|
||||
|
@ -126,7 +126,8 @@ export default {
|
||||
fieldnames.includes('hideGetStarted') ||
|
||||
fieldnames.includes('displayPrecision') ||
|
||||
fieldnames.includes('enableDiscounting') ||
|
||||
fieldnames.includes('enableInventory')
|
||||
fieldnames.includes('enableInventory') ||
|
||||
fieldnames.includes('enableBarcodes')
|
||||
) {
|
||||
this.showReloadToast();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
}
|
||||
|
||||
* {
|
||||
outline-color: theme('colors.pink.500');
|
||||
font-variation-settings: 'slnt' 0deg;
|
||||
}
|
||||
.italic {
|
||||
|
@ -7,7 +7,7 @@ import { isPesa } from 'fyo/utils';
|
||||
import {
|
||||
BaseError,
|
||||
DuplicateEntryError,
|
||||
LinkValidationError
|
||||
LinkValidationError,
|
||||
} from 'fyo/utils/errors';
|
||||
import { Money } from 'pesa';
|
||||
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
|
||||
@ -85,7 +85,7 @@ export function convertPesaValuesToFloat(obj: Record<string, unknown>) {
|
||||
return;
|
||||
}
|
||||
|
||||
obj[key] = (value as Money).float;
|
||||
obj[key] = value.float;
|
||||
});
|
||||
}
|
||||
|
||||
|
1386
translations/gu.csv
1386
translations/gu.csv
File diff suppressed because it is too large
Load Diff
@ -83,7 +83,7 @@ export function getListFromMap<T>(map: Record<string, T>): T[] {
|
||||
return Object.keys(map).map((n) => map[n]);
|
||||
}
|
||||
|
||||
export function getIsNullOrUndef(value: unknown): boolean {
|
||||
export function getIsNullOrUndef(value: unknown): value is null | undefined {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user