2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 22:58:28 +00:00
This commit is contained in:
Viral Ghelani 2023-01-20 14:36:05 +05:30
commit a2d44a686b
21 changed files with 266 additions and 35 deletions

View File

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

View File

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

View File

@ -120,6 +120,7 @@ export class Item extends Doc {
!this.fyo.singles.AccountingSettings?.enableInventory ||
this.itemType !== 'Product' ||
(this.inserted && !this.trackItem),
barcode: () => !this.fyo.singles.InventorySettings?.enableBarcodes,
};
readOnly: ReadOnlyMap = {

View File

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

View File

@ -9,6 +9,7 @@ export class InventorySettings extends Doc {
valuationMethod?: ValuationMethod;
stockReceivedButNotBilled?: string;
costOfGoodsSold?: string;
enableBarcodes?: boolean;
static filters: FiltersMap = {
stockInHand: () => ({

View File

@ -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';
@ -92,4 +92,8 @@ export class StockMovement extends Transfer {
static getActions(fyo: Fyo): Action[] {
return [getLedgerLinkAction(fyo, true)];
}
async addItem(name: string) {
return await addItem(name, this);
}
}

View File

@ -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';
@ -232,4 +232,8 @@ export abstract class StockTransfer extends Transfer {
role: doc.isSales ? 'Customer' : 'Supplier',
}),
};
async addItem(name: string) {
return await addItem(name, this);
}
}

View File

@ -106,6 +106,12 @@
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
},
{
"fieldname": "barcode",
"label": "Barcode",
"fieldtype": "Data",
"placeholder": "Barcode"
},
{
"fieldname": "trackItem",
"label": "Track Item",
@ -122,6 +128,7 @@
"description",
"incomeAccount",
"expenseAccount",
"barcode",
"hsnCode",
"trackItem"
],

View File

@ -45,6 +45,11 @@
"label": "Cost Of Goods Sold Acc.",
"fieldtype": "Link",
"target": "Account"
},
{
"fieldname": "enableBarcodes",
"label": "Enable Barcodes",
"fieldtype": "Check"
}
]
}

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

View File

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

View File

@ -4,7 +4,7 @@
class="
fixed
top-0
left-0
start-0
w-screen
h-screen
z-20
@ -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: {

View File

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

View File

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

View File

@ -36,15 +36,16 @@
class="
absolute
bottom-0
left-0
start-0
text-gray-600
bg-gray-100
rounded
rtl-rotate-180
p-1
m-4
opacity-0
hover:opacity-100 hover:shadow-md
"
@click="sidebar = !sidebar"
>
<feather-icon name="chevrons-right" class="w-4 h-4" />
@ -76,6 +77,11 @@ export default {
transform: translateX(calc(-1 * var(--w-sidebar)));
width: 0px;
}
[dir='rtl'] .sidebar-leave-to {
opacity: 0;
transform: translateX(calc(1 * var(--w-sidebar)));
width: 0px;
}
.sidebar-enter-to,
.sidebar-leave-from {

View File

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

View File

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

View File

@ -31,6 +31,7 @@
/>
<Modal :open-modal="openExportModal" @closemodal="openExportModal = false">
<ExportWizard
class="w-form"
:schema-name="schemaName"
:title="pageTitle"
:list-filters="listFilters"

View File

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

View File

@ -7,6 +7,7 @@
}
* {
outline-color: theme('colors.pink.500');
font-variation-settings: 'slnt' 0deg;
}
.italic {

View File

@ -38,7 +38,7 @@ Accounts,Comptes,
"Add a remark","Ajouter une remarque",
"Add invoice terms","Ajouter votre politique de vente",
"Add products or services that you buy from your suppliers","Ajoutez des produits ou services que vous achetez à vos fournisseurs",
"Add products or services that you sell to your customers","Ajouter les produits ou services que vous vendez à vos clients.",
"Add products or services that you sell to your customers","Ajouter les produits ou services que vous vendez à vos clients",
Address,Adresse,
"Address Display","Affichage de l'adresse",
"Address Line 1","Ligne d'adresse 1",
@ -76,7 +76,7 @@ Buildings,Bâtiments,
Business,Entreprise,
Cancel,Annuler,
"Cancel ${0} ${1}?","Annuler ${0} ${1} ?",
Cancelled,Anulé,
Cancelled,Annulé,
"Cannot Delete","Impossible à supprimer",
"Cannot delete ${0} ${1} because of linked entries.","Impossible de supprimer ${0} ${1} à cause des entrées liées.",
"Capital Equipments","Biens d'équipement",
@ -143,8 +143,8 @@ Credit,Crédit,
"Credit Card Entry","Entrée Carte de Crédit",
"Credit Note",Avoir,
Creditors,Créanciers,
Currency,Monnaie,
"Currency Name","Nom de la monnaie",
Currency,Devise,
"Currency Name","Nom de la devise",
Current,Actuel,
"Current Assets","Actifs courants",
"Current Liabilities","Passifs courants",
@ -153,11 +153,11 @@ Customer,Client,
"Customer Created","Client créé",
"Customer Currency","Monnaie du client",
Customers,Clients,
Customise,Personnalisez,
"Customize your invoices by adding a logo and address details","Customisez vos factures en y ajoutant votre logo et adresse",
Customise,Personnaliser,
"Customize your invoices by adding a logo and address details",Personnalisez vos factures en y ajoutant votre logo et adresse,
Dashboard,"Tableau de bord",
"Data Import","Importation de données",
"Database file: ${0}","Fichier de base de donnée : ${0}",
"Database file: ${0}","Fichier de base de données : ${0}",
Date,,
"Date Format","Format de la date",
Debit,Débit,
@ -176,7 +176,7 @@ Details,Détails,
"Direct Income","Revenu direct",
Discount,Réduction,
"Discount Account","Compte des réductions",
"Discount Account is not set.","Le compte des réductions n'est pas défini",
"Discount Account is not set.","Le compte des réductions n'est pas défini.",
"Discount After Tax","Réduction après taxes",
"Discount Amount","Montant de la réduction",
"Discount Amount (${0}) cannot be greated than Amount (${1}).","Le montant de la réduction (${0}) ne peut pas être plus grand que le montant (${1}).",
@ -189,7 +189,7 @@ Discounts,"Réductions",
"Display Logo in Invoice","Afficher le logo sur la facture",
"Display Precision","Précision de l'affichage",
"Display Precision should have a value between 0 and 9.","La précision de l'affichage doit avoir une valeur comprise entre 0 et 9.",
"Dividends Paid","Dividendes versés",
"Dividends Paid","Dividendes versées",
Docs,Documents,
Documentation,Documentation,
"Does Not Contain","Ne contient pas",
@ -222,7 +222,7 @@ Expenses,Dépenses,
"Expenses Included In Valuation","Dépenses incluses dans la valorisation ",
Export,Exporter,
"Export Failed","Export Échoué",
"Export Successful","Export réussie",
"Export Successful","Export réussi",
Fax,Fax,
Field,Champ,
Fieldname,"Nom du champ",
@ -377,13 +377,13 @@ Orange,,
Organisation,Organisation,
Outflow,Dépenses,
"Outstanding Amount","Montant impayé",
"Pad Zeros","Remplir de zéros",
"Pad Zeros","Remplir de zéros",
Page,,
Paid,Payé,
Parent,,
"Parent Account","Compte parent",
Party,Partie,
"Patch Run","Éxecuter les correctifs",
"Patch Run","Exécuter les correctifs",
Pay,Payer,
Payable,Payable,
Payment,Paiement,
@ -434,7 +434,7 @@ Purchases,Achats,
Purple,Violet,
Quantity,Quantité,
Quarterly,Trimestriel,
Quarters,Trimestre,
Quarters,Trimestres,
Rate,Tarif,
"Rate (${0}) cannot be less zero.","Le Tarif (${0}) ne peut pas être inférieur à zéro.",
"Rate (Company Currency)","Tarif (devise utilisée par la société)",
@ -584,7 +584,7 @@ Terms,Conditions,
"This action is permanent","Cette action est permanente",
"This action is permanent and will cancel the following payment: ${0}","Cette action est permanente et annulera le paiement suivant : ${0}",
"This action is permanent and will cancel the following payments: ${0}","Cette action est permanente et annulera les paiements suivants : ${0}",
"This action is permanent and will delete associated ledger entries.","Cette action est permanente et supprimera les écritures dans le registre associées.",
"This action is permanent and will delete associated ledger entries.","Cette action est permanente et supprimera les écritures dans le registre associés.",
"This action is permanent.","Cette action est permanente",
"Times New Roman",,
"To Account","Au compte",

1 ${0} ${1} already exists. ${0} ${1} existe déjà.
38 Add a remark Ajouter une remarque
39 Add invoice terms Ajouter votre politique de vente
40 Add products or services that you buy from your suppliers Ajoutez des produits ou services que vous achetez à vos fournisseurs
41 Add products or services that you sell to your customers Ajouter les produits ou services que vous vendez à vos clients. Ajouter les produits ou services que vous vendez à vos clients
42 Address Adresse
43 Address Display Affichage de l'adresse
44 Address Line 1 Ligne d'adresse 1
76 Business Entreprise
77 Cancel Annuler
78 Cancel ${0} ${1}? Annuler ${0} ${1} ?
79 Cancelled Anulé Annulé
80 Cannot Delete Impossible à supprimer
81 Cannot delete ${0} ${1} because of linked entries. Impossible de supprimer ${0} ${1} à cause des entrées liées.
82 Capital Equipments Biens d'équipement
143 Credit Card Entry Entrée Carte de Crédit
144 Credit Note Avoir
145 Creditors Créanciers
146 Currency Monnaie Devise
147 Currency Name Nom de la monnaie Nom de la devise
148 Current Actuel
149 Current Assets Actifs courants
150 Current Liabilities Passifs courants
153 Customer Created Client créé
154 Customer Currency Monnaie du client
155 Customers Clients
156 Customise Personnalisez Personnaliser
157 Customize your invoices by adding a logo and address details Customisez vos factures en y ajoutant votre logo et adresse Personnalisez vos factures en y ajoutant votre logo et adresse
158 Dashboard Tableau de bord
159 Data Import Importation de données
160 Database file: ${0} Fichier de base de donnée : ${0} Fichier de base de données : ${0}
161 Date
162 Date Format Format de la date
163 Debit Débit
176 Direct Income Revenu direct
177 Discount Réduction
178 Discount Account Compte des réductions
179 Discount Account is not set. Le compte des réductions n'est pas défini Le compte des réductions n'est pas défini.
180 Discount After Tax Réduction après taxes
181 Discount Amount Montant de la réduction
182 Discount Amount (${0}) cannot be greated than Amount (${1}). Le montant de la réduction (${0}) ne peut pas être plus grand que le montant (${1}).
189 Display Logo in Invoice Afficher le logo sur la facture
190 Display Precision Précision de l'affichage
191 Display Precision should have a value between 0 and 9. La précision de l'affichage doit avoir une valeur comprise entre 0 et 9.
192 Dividends Paid Dividendes versés Dividendes versées
193 Docs Documents
194 Documentation Documentation
195 Does Not Contain Ne contient pas
222 Expenses Included In Valuation Dépenses incluses dans la valorisation
223 Export Exporter
224 Export Failed Export Échoué
225 Export Successful Export réussie Export réussi
226 Fax Fax
227 Field Champ
228 Fieldname Nom du champ
377 Organisation Organisation
378 Outflow Dépenses
379 Outstanding Amount Montant impayé
380 Pad Zeros Remplir de zéros Remplir de zéros
381 Page
382 Paid Payé
383 Parent
384 Parent Account Compte parent
385 Party Partie
386 Patch Run Éxecuter les correctifs Exécuter les correctifs
387 Pay Payer
388 Payable Payable
389 Payment Paiement
434 Purple Violet
435 Quantity Quantité
436 Quarterly Trimestriel
437 Quarters Trimestre Trimestres
438 Rate Tarif
439 Rate (${0}) cannot be less zero. Le Tarif (${0}) ne peut pas être inférieur à zéro.
440 Rate (Company Currency) Tarif (devise utilisée par la société)
584 This action is permanent Cette action est permanente
585 This action is permanent and will cancel the following payment: ${0} Cette action est permanente et annulera le paiement suivant : ${0}
586 This action is permanent and will cancel the following payments: ${0} Cette action est permanente et annulera les paiements suivants : ${0}
587 This action is permanent and will delete associated ledger entries. Cette action est permanente et supprimera les écritures dans le registre associées. Cette action est permanente et supprimera les écritures dans le registre associés.
588 This action is permanent. Cette action est permanente
589 Times New Roman
590 To Account Au compte