From 0e49c49ca4d20e01172eeab5fd0a4b9ba63a3c78 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 14:07:59 +0530 Subject: [PATCH 1/2] Create typed customers on sale save and capture GST place of supply --- api/src/controllers/customers.controller.js | 8 ++ api/src/helpers/gst-place.js | 22 ++- api/src/models/sale.model.js | 3 + api/src/repositories/sale.repository.js | 24 ++-- api/src/services/sale-customer.js | 131 ++++++++++++++++++ api/src/services/sale.service.js | 86 ++++++++---- api/tests/unit/helpers/gst-place.test.js | 23 ++- .../unit/repositories/gstr1-export.test.js | 19 +++ .../services/sale-customer.integration.cjs | 31 +++++ api/tests/unit/services/sale.service.test.js | 73 +++++++++- frontend/modules/sales_write.html | 40 +++--- frontend/static/script/js/modules/js/sales.js | 130 +++++++++++------ frontend/static/style/css/theme-variables.css | 13 ++ tests/sales-customer-gst.test.js | 10 ++ tests/sales-customer-popup.test.js | 41 +++--- 15 files changed, 536 insertions(+), 118 deletions(-) create mode 100644 api/src/services/sale-customer.js create mode 100644 api/tests/unit/services/sale-customer.integration.cjs create mode 100644 tests/sales-customer-gst.test.js diff --git a/api/src/controllers/customers.controller.js b/api/src/controllers/customers.controller.js index 6ad6ca54c..d1f217ce5 100644 --- a/api/src/controllers/customers.controller.js +++ b/api/src/controllers/customers.controller.js @@ -428,6 +428,14 @@ class CustomerController extends BaseController { return this.sendError(res, 'Search term is required', 400, null); } + await require('../services/sale-customer').recover( + await BaseModel.getDb(), + { + branchId: new ObjectId(branchId), + license: new ObjectId(BaseModel.license), + }, + searchTerm + ); const options = { page, limit, branchId }; // Call service diff --git a/api/src/helpers/gst-place.js b/api/src/helpers/gst-place.js index acfa037c9..b9f5fa992 100644 --- a/api/src/helpers/gst-place.js +++ b/api/src/helpers/gst-place.js @@ -22,8 +22,28 @@ function customerState(data, branch, customer) { const isDefault = !data.customer_id || (branch.default_customer && String(data.customer_id) === String(branch.default_customer)); + const explicit = String(data.customer_state || '').trim(); + if (explicit) return explicit; if (isDefault) return supplier; return String(data.customer_state || customer?.state || '').trim() || supplier; } -module.exports = { stateKey, isInterState, customerState }; +function placeOfSupply(data, branch, customer) { + return String(data.place_of_supply || '').trim() || customerState(data, branch, customer); +} +function validate(data) { + for (const field of ['customer_state', 'place_of_supply']) { + if (data[field] && !gststate.some((row) => row.id === stateKey(data[field]))) + throw new Error('Select a valid Indian state.'); + } + const gstin = String(data.customer_gst_number || '') + .trim() + .toUpperCase(); + if (gstin && !/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/.test(gstin)) + throw new Error('Enter a valid customer GSTIN.'); + if (gstin && !data.customer_state) + data.customer_state = gststate.find((row) => row.id === gstin.slice(0, 2))?.value || ''; + if (gstin && data.customer_state && gstin.slice(0, 2) !== stateKey(data.customer_state)) + throw new Error('GSTIN and customer state do not match.'); +} +module.exports = { stateKey, isInterState, customerState, placeOfSupply, validate }; diff --git a/api/src/models/sale.model.js b/api/src/models/sale.model.js index 79338ac87..63138ae1a 100644 --- a/api/src/models/sale.model.js +++ b/api/src/models/sale.model.js @@ -400,6 +400,8 @@ const saleSchema = new mongoose.Schema( trim: true, }, customer_phone: String, + place_of_supply: { type: String, trim: true }, + customer_creation_pending: { type: Boolean, default: false }, customer_email: { type: String, trim: true, @@ -1148,6 +1150,7 @@ class LegacySaleModel { customer_phone: { type: 'String', select: true }, customer_email: { type: 'String', select: true }, customer_state: { type: 'String', select: true }, + place_of_supply: { type: 'String', select: true }, customer_country: { type: 'String', select: true }, customer_gst_type: { type: 'String', select: true }, customer_gst_number: { type: 'String', select: true }, diff --git a/api/src/repositories/sale.repository.js b/api/src/repositories/sale.repository.js index 67d14b962..d30dda77c 100644 --- a/api/src/repositories/sale.repository.js +++ b/api/src/repositories/sale.repository.js @@ -136,6 +136,7 @@ const gstStateCode = (gstin, stateName) => { .trim() .toLowerCase(); if (!name) return ''; + if (/^\d{2}$/.test(name)) return name; if (!_gstStateByName) { _gstStateByName = new Map(); try { @@ -946,7 +947,9 @@ class SalesRepository { ); if (perLineGstAmount > 0) { - const customerState = (doc.customer_state || '').toString().trim(); + const customerState = (doc.place_of_supply || doc.customer_state || '') + .toString() + .trim(); const branchState = (branchDoc?.state || '').toString().trim(); const isInterState = require('../helpers/gst-place').isInterState( @@ -4110,6 +4113,7 @@ class SalesRepository { customer_phone: 1, customer_email: 1, customer_state: 1, + place_of_supply: 1, customer_country: 1, customer_gst_type: 1, customer_gst_number: 1, @@ -4466,6 +4470,7 @@ class SalesRepository { customer_phone: 1, customer_email: 1, customer_state: 1, + place_of_supply: 1, customer_country: 1, customer_gst_type: 1, customer_gst_number: 1, @@ -6348,7 +6353,7 @@ class SalesRepository { _id: { item_sales_id: '$sales_id', item_date: '$date', - item_customer_state: '$customer_state', + item_customer_state: { $ifNull: ['$place_of_supply', '$customer_state'] }, item_customer_gst_number: '$customer_gst_number', item_tax: '$items.tax', item_subtotal: '$items_subtotal', @@ -6398,7 +6403,7 @@ class SalesRepository { _id: { return_sales_id: '$sales_id', return_sales_date: '$date', - return_customer_state: '$customer_state', + return_customer_state: { $ifNull: ['$place_of_supply', '$customer_state'] }, // the credit-note rows printed a placeholder GSTIN; carry // the real one so the 9B table shows the actual customer return_customer_gst_number: '$customer_gst_number', @@ -6502,7 +6507,7 @@ class SalesRepository { $and: [ { branch_id: branchId, - customer_state: { $not: { $eq: branchState } }, + $expr: { $ne: [{ $ifNull: ['$place_of_supply', '$customer_state'] }, branchState] }, customer_gst_type: { $in: ['consumer'] }, }, { @@ -6522,7 +6527,7 @@ class SalesRepository { _id: { item_sales_id: '$sales_id', item_date: '$date', - item_customer_state: '$customer_state', + item_customer_state: { $ifNull: ['$place_of_supply', '$customer_state'] }, item_customer_gst_number: '$customer_gst_number', item_tax: '$items.tax', item_subtotal: '$items_subtotal', @@ -6904,7 +6909,7 @@ class SalesRepository { $and: [ { branch_id: new ObjectId(branch_id), - customer_state: { $not: { $eq: branch_state } }, + $expr: { $ne: [{ $ifNull: ['$place_of_supply', '$customer_state'] }, branch_state] }, customer_gst_type: { $in: ['consumer', 'composite'] }, }, { @@ -6922,7 +6927,7 @@ class SalesRepository { { $group: { _id: { - item_customer_state: '$customer_state', + item_customer_state: { $ifNull: ['$place_of_supply', '$customer_state'] }, customer_gst_type: '$customer_gst_type', }, total_amount: { $sum: '$items_subtotal' }, @@ -7132,6 +7137,7 @@ class SalesRepository { }, date: { $first: '$date' }, customer_state: { $first: '$customer_state' }, + place_of_supply: { $first: '$place_of_supply' }, invoice_value: { $first: '$sales_total' }, items_value: { $sum: '$items.total_amount' }, igst: { $sum: '$items.igst_tax' }, @@ -7167,7 +7173,9 @@ class SalesRepository { // Invoice value: the sale's own total, not a sum of lines - // charges, round-off and bill-level discounts belong in it. val: round(Number(row.invoice_value) || 0), - pos: gstStateCode(ctin, row.customer_state), + pos: row.place_of_supply + ? gstStateCode('', row.place_of_supply) + : gstStateCode(ctin, row.customer_state), rchrg: 'N', inv_typ: 'R', itms: [], diff --git a/api/src/services/sale-customer.js b/api/src/services/sale-customer.js new file mode 100644 index 000000000..d8f9c14e9 --- /dev/null +++ b/api/src/services/sale-customer.js @@ -0,0 +1,131 @@ +'use strict'; +const { ObjectId } = require('mongodb'); +const { createHash } = require('crypto'); +const validator = require('validator'); +const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +function customerId(scope, name) { + return new ObjectId( + createHash('sha256') + .update( + JSON.stringify([ + 'sale-customer', + String(scope.license), + String(scope.branchId), + name.trim().replace(/\s+/g, ' ').toLowerCase(), + ]) + ) + .digest('hex') + .slice(0, 24) + ); +} + +// Planning is read-only: failed validation, previews and abandoned baskets +// must never create contacts. The committed sale is the durable recovery record. +async function plan(db, scope, data) { + if (data.create_customer_on_save !== true || data.customer_id) return null; + const name = String(data.customer_name || '') + .trim() + .replace(/\s+/g, ' '); + if (!name) return null; + if (name.length > 200) throw new Error('Customer name is too long.'); + for (const [key, limit] of [ + ['phone', 40], + ['email', 254], + ['address', 500], + ]) { + if (String(data['customer_' + key] || '').length > limit) + throw new Error('Customer ' + key + ' is too long.'); + } + const email = String(data.customer_email || '').trim(); + if (email && !validator.isEmail(email)) throw new Error('Enter a valid customer email.'); + const collection = db.collection('customers'); + const matches = await collection + .find({ + license: scope.license, + branch_id: scope.branchId, + is_deleted: { $ne: true }, + name: new RegExp('^' + escape(name) + '$', 'i'), + }) + .limit(2) + .toArray(); + if (matches.length > 1) + throw new Error('More than one customer has this name. Select the correct customer.'); + if (matches.length) return { id: matches[0]._id, pending: false, customer: matches[0] }; + const id = customerId(scope, name); + if (await collection.findOne({ _id: id, is_deleted: true })) + throw new Error('This customer was deleted. Restore the customer or use a different name.'); + if ( + email && + (await collection.findOne({ license: scope.license, email, is_deleted: { $ne: true } })) + ) + throw new Error('This email belongs to a saved customer. Select that customer.'); + return { id, pending: true }; +} + +async function finish(db, scope, saleId) { + const sales = db.collection('sales'); + const sale = await sales.findOne({ + _id: new ObjectId(saleId), + license: scope.license, + branch_id: scope.branchId, + customer_creation_pending: true, + }); + if (!sale) return; + const id = customerId(scope, sale.customer_name); + if (String(id) !== String(sale.customer_id)) + throw new Error('Customer creation identity mismatch.'); + const now = new Date(); + const customer = { + _id: id, + name: sale.customer_name, + license: scope.license, + branch_id: scope.branchId, + branch_name: sale.branch_name || '', + branch_access: [{ branch_id: scope.branchId, branch_name: sale.branch_name || '' }], + phone: sale.customer_phone || '', + address: sale.customer_address || '', + state: sale.customer_state || '', + country: sale.customer_country || '', + gst_type: sale.customer_gst_type || 'consumer', + gst_number: sale.customer_gst_number || '', + balance: 0, + wallet: 0, + partial_balance: false, + is_deleted: false, + created_date: now, + updated_date: now, + created_by: sale.created_by || '', + created_by_id: sale.created_by_id, + }; + if (sale.customer_email) customer.email = sale.customer_email; + // Stable across concurrent saves and offline tills; never overwrite details, + // balances, or a later deletion when replaying a saved sale. + try { + await db + .collection('customers') + .updateOne({ _id: id }, { $setOnInsert: customer }, { upsert: true }); + } catch (error) { + if (error.code !== 11000 || !(await db.collection('customers').findOne({ _id: id }))) + throw error; + } + await sales.updateOne( + { _id: sale._id, license: scope.license, branch_id: scope.branchId }, + { $set: { customer_creation_pending: false, updated_date: now } } + ); +} + +async function recover(db, scope, query) { + if (!scope.license || !scope.branchId) return; + const pending = await db + .collection('sales') + .find({ + license: scope.license, + branch_id: scope.branchId, + customer_creation_pending: true, + customer_name: new RegExp(escape(String(query).slice(0, 200)), 'i'), + }) + .limit(20) + .toArray(); + for (const sale of pending) await finish(db, scope, sale._id); +} +module.exports = { customerId, plan, finish, recover }; diff --git a/api/src/services/sale.service.js b/api/src/services/sale.service.js index bd7963e02..8fc17a29f 100644 --- a/api/src/services/sale.service.js +++ b/api/src/services/sale.service.js @@ -284,25 +284,33 @@ const processSale = async ( return { status: false, message: ERROR_MESSAGES.BRANCH_LICENSE_REQUIRED }; } - const savedAnswer = (saleId, saleNumber, duplicate = false) => ({ - status: true, - data: { - _id: saleId, - sales_id: saleId, - sale_number: saleNumber, - sms: context.branchSettings?.sales_sms || false, - whatsapp: context.branchSettings?.whatsapp_receipt || false, - print: context.branchSettings?.printall || false, - mail: context.branchSettings?.sales_mail || false, - waring: 'success', - name: (data.customer_name || '').trim(), - phone: (data.customer_phone || '').trim(), - customer_balance: duplicate ? 0 : customer ? customer.balance || 0 : 0, - country_sort: context.branchSettings?.sortname || 'in', - ...(duplicate ? { duplicate: true } : {}), - }, - message: 'Sale saved successfully', - }); + const savedAnswer = async (saleId, saleNumber, duplicate = false) => { + if (data.create_customer_on_save === true) + await require('./sale-customer').finish( + await BaseModel.getDb(), + { branchId, license: licenseId }, + saleId + ); + return { + status: true, + data: { + _id: saleId, + sales_id: saleId, + sale_number: saleNumber, + sms: context.branchSettings?.sales_sms || false, + whatsapp: context.branchSettings?.whatsapp_receipt || false, + print: context.branchSettings?.printall || false, + mail: context.branchSettings?.sales_mail || false, + waring: 'success', + name: (data.customer_name || '').trim(), + phone: (data.customer_phone || '').trim(), + customer_balance: duplicate ? 0 : customer ? customer.balance || 0 : 0, + country_sort: context.branchSettings?.sortname || 'in', + ...(duplicate ? { duplicate: true } : {}), + }, + message: 'Sale saved successfully', + }; + }; // A retry returns its committed result before current catalogue changes // can invalidate the original, already accepted request. @@ -313,7 +321,7 @@ const processSale = async ( String(userId || ''), extensionSubmission || submittedPayload ); - if (existing) return savedAnswer(existing._id, existing.sales_id, true); + if (existing) return await savedAnswer(existing._id, existing.sales_id, true); } // 2. Check Plan / Max Sales (Skipped for now, assuming valid plan) @@ -422,12 +430,27 @@ const processSale = async ( existingSale, data.outlet_revision ); + const customerPlan = + data.create_customer_on_save === true + ? await require('./sale-customer').plan( + await BaseModel.getDb(), + { branchId, license: licenseId }, + data + ) + : null; + if (customerPlan) data.customer_id = String(customerPlan.id); const pricingBranch = context.branchSettings || {}; const pricingCustomer = data.customer_id ? await customerRepository.findById(data.customer_id) : null; const gstPlace = require('../helpers/gst-place'); const supplierState = context.branchState || pricingBranch.state || ''; + if (pricingBranch.indian_gst === 'gst_on') gstPlace.validate(data); + const salePlaceOfSupply = gstPlace.placeOfSupply( + data, + { ...pricingBranch, state: supplierState }, + pricingCustomer + ); const saleCustomerState = gstPlace.customerState( data, { ...pricingBranch, state: supplierState }, @@ -669,7 +692,7 @@ const processSale = async ( const indianGstSetting = context.branchSettings?.indian_gst || 'gst_off'; - const interPlace = gstPlace.isInterState(supplierState, saleCustomerState); + const interPlace = gstPlace.isInterState(supplierState, salePlaceOfSupply); if (indianGstSetting === 'gst_on' && gstValue > 0) { if (interPlace) { igst_value = gstValue; @@ -1262,6 +1285,10 @@ const processSale = async ( referrer_name: customer ? customer.referrer_name : '', customer_id: data.customer_id ? new ObjectId(data.customer_id) : null, customer_name: (data.customer_name || '').trim(), + customer_creation_pending: + customerPlan?.pending === true || + (existingSale?.customer_creation_pending === true && + String(existingSale.customer_id) === String(data.customer_id)), customer_address: (data.customer_address || '').trim(), customer_phone: (data.customer_phone || '').trim(), ...(outlet @@ -1282,6 +1309,7 @@ const processSale = async ( : {}), customer_email: (data.customer_email || '').trim(), customer_state: saleCustomerState, + place_of_supply: salePlaceOfSupply, customer_country: (data.customer_country || '').trim(), country_sort: finalCountrySort, customer_gst_type: (data.customer_gst_type || '').trim(), @@ -1445,7 +1473,7 @@ const processSale = async ( extensionSubmission || submittedPayload, finalSaleData ); - if (existing) return savedAnswer(existing._id, existing.sales_id, true); + if (existing) return await savedAnswer(existing._id, existing.sales_id, true); } let seatingAttempt = null; @@ -1463,7 +1491,7 @@ const processSale = async ( { branchId, license: licenseId }, { actor: String(userId || ''), request_id: data.idempotencyKey, payload: data } ); - if (existing) return savedAnswer(existing._id, existing.sales_id, true); + if (existing) return await savedAnswer(existing._id, existing.sales_id, true); } // Inventory Verification BEFORE Insert (PHP lines 653-690) @@ -1576,7 +1604,11 @@ const processSale = async ( if (seatingAttempt?.existing) { for (const reservation of stockReservations.values()) await itemRepository.updateStock(reservation.itemId, reservation.quantity); - return savedAnswer(seatingAttempt.existing._id, seatingAttempt.existing.sales_id, true); + return await savedAnswer( + seatingAttempt.existing._id, + seatingAttempt.existing.sales_id, + true + ); } } // If the unique bill-number index catches a one-in-a-million clash, @@ -1596,7 +1628,7 @@ const processSale = async ( String(userId || ''), extensionSubmission || submittedPayload ); - if (existing) return savedAnswer(existing._id, existing.sales_id, true); + if (existing) return await savedAnswer(existing._id, existing.sales_id, true); } if (seatingAttempt && error.code === 11000) { const existing = await seatingDb.collection('sales').findOne({ @@ -1605,7 +1637,7 @@ const processSale = async ( license: licenseId, seating_request_id: seatingAttempt.claim.id, }); - if (existing) return savedAnswer(existing._id, existing.sales_id, true); + if (existing) return await savedAnswer(existing._id, existing.sales_id, true); } throw error; } @@ -2047,7 +2079,7 @@ const processSale = async ( console.warn('[takeaway] Completion reconciliation pending:', error.message); } } - return savedAnswer(saleId, salePrefixedId); + return await savedAnswer(saleId, salePrefixedId); } catch (error) { console.error('processSale Error:', error); if (error.name === 'DocumentNotFoundError') { diff --git a/api/tests/unit/helpers/gst-place.test.js b/api/tests/unit/helpers/gst-place.test.js index cddd9d081..40997b346 100644 --- a/api/tests/unit/helpers/gst-place.test.js +++ b/api/tests/unit/helpers/gst-place.test.js @@ -14,10 +14,27 @@ test.each([ test('walk-in and configured default customers use the branch state', () => { const branch = { state: 'Tamil Nadu', default_customer: 'walkin-id' }; expect(customerState({}, branch)).toBe('Tamil Nadu'); - expect(customerState({ customer_id: 'walkin-id', customer_state: '29' }, branch)).toBe( - 'Tamil Nadu' - ); + expect(customerState({ customer_id: 'walkin-id', customer_state: '29' }, branch)).toBe('29'); expect(customerState({ customer_id: 'named-id' }, branch, { state: '29' })).toBe('29'); expect(customerState({ customer_id: 'named-id' }, branch)).toBe('Tamil Nadu'); expect(stateKey(' 33 ')).toBe('33'); }); + +test('name-only state and delivery override determine place of supply', () => { + const gst = require('../../../src/helpers/gst-place'); + expect(gst.customerState({ customer_state: 'Karnataka' }, { state: 'Goa' })).toBe('Karnataka'); + expect( + gst.placeOfSupply({ customer_state: 'Goa', place_of_supply: '29' }, { state: 'Goa' }) + ).toBe('29'); + expect(gst.placeOfSupply({}, { state: 'Goa' })).toBe('Goa'); + expect(() => + gst.validate({ customer_state: 'Goa', customer_gst_number: '29ABCDE1234F1Z5' }) + ).toThrow(/do not match/); + expect(() => + gst.validate({ + customer_state: 'Karnataka', + place_of_supply: 'Goa', + customer_gst_number: '29ABCDE1234F1Z5', + }) + ).not.toThrow(); +}); diff --git a/api/tests/unit/repositories/gstr1-export.test.js b/api/tests/unit/repositories/gstr1-export.test.js index e61e3363b..b3fae0c26 100644 --- a/api/tests/unit/repositories/gstr1-export.test.js +++ b/api/tests/unit/repositories/gstr1-export.test.js @@ -206,6 +206,25 @@ describe('GSTR-1 B2B export', () => { expect(res.data.find((r) => r.ctin === '').inv[0].pos).toBe('03'); }); + test('explicit delivery state overrides the customer GSTIN state', async () => { + rows([ + { + _id: { sales_id: 'INV-POS', ctin: TN, rate: 18 }, + date: new Date('2026-08-05T00:00:00Z'), + customer_state: 'Tamil Nadu', + place_of_supply: 'Punjab', + invoice_value: 1180, + items_value: 1180, + igst: 180, + cgst: 0, + sgst: 0, + }, + ]); + const res = await run(); + expect(res.data[0].inv[0].pos).toBe('03'); + expect(res.data[0].ctin).toBe(TN); + }); + test('invoice date is rendered DD-MM-YYYY as the tool expects', async () => { rows([ { diff --git a/api/tests/unit/services/sale-customer.integration.cjs b/api/tests/unit/services/sale-customer.integration.cjs new file mode 100644 index 000000000..7d117f756 --- /dev/null +++ b/api/tests/unit/services/sale-customer.integration.cjs @@ -0,0 +1,31 @@ +const {test,before,after,beforeEach}=require('node:test'); +const assert=require('node:assert/strict'); +const {MongoMemoryServer}=require('mongodb-memory-server'); +const {MongoClient,ObjectId}=require('mongodb'); +const service=require('../../../src/services/sale-customer'); +let server,client,db; +const scope={branchId:new ObjectId(),license:new ObjectId()}; +const input={create_customer_on_save:true,customer_name:'New Buyer',customer_state:'Goa'}; +before(async()=>{server=await MongoMemoryServer.create();client=await MongoClient.connect(server.getUri());db=client.db('sale_customer_test');}); +after(async()=>{await client?.close();await server?.stop();}); +beforeEach(async()=>{await db.dropDatabase();}); +async function saved(data=input){const planned=await service.plan(db,scope,data);const sale={_id:new ObjectId(),branch_id:scope.branchId,license:scope.license,customer_id:planned.id,customer_name:data.customer_name,customer_phone:data.customer_phone||'',customer_state:data.customer_state,customer_creation_pending:planned.pending};await db.collection('sales').insertOne(sale);return sale;} +test('planning an abandoned/failed sale writes no customer; successful save creates name-only customer',async()=>{ + const planned=await service.plan(db,scope,input);assert(planned.pending);assert.equal(await db.collection('customers').countDocuments(),0); + const sale=await saved();await service.finish(db,scope,sale._id);const customer=await db.collection('customers').findOne({_id:sale.customer_id});assert.equal(customer.name,'New Buyer');assert.equal(customer.phone,'');assert.equal(customer.state,'Goa');assert(customer.updated_date instanceof Date); + assert.equal((await db.collection('sales').findOne({_id:sale._id})).customer_creation_pending,false); +}); +test('concurrent saves/retries reuse stable id and do not overwrite edited contact data',async()=>{ + const a=await saved(),b=await saved();assert.equal(String(a.customer_id),String(b.customer_id));await Promise.all([service.finish(db,scope,a._id),service.finish(db,scope,b._id)]);assert.equal(await db.collection('customers').countDocuments(),1); + await db.collection('customers').updateOne({_id:a.customer_id},{$set:{phone:'changed',balance:99}});await service.finish(db,scope,a._id);assert.equal((await db.collection('customers').findOne({_id:a.customer_id})).balance,99); +}); +test('exact existing name is reused case-insensitively; ambiguous names require selection',async()=>{ + const id=new ObjectId();await db.collection('customers').insertOne({_id:id,branch_id:scope.branchId,license:scope.license,name:'NEW BUYER'});assert.equal(String((await service.plan(db,scope,input)).id),String(id));await db.collection('customers').insertOne({branch_id:scope.branchId,license:scope.license,name:'New Buyer'});await assert.rejects(service.plan(db,scope,input),/Select the correct/); +}); +test('committed sale recovery works after restart and never crosses branches or revives deletion',async()=>{ + const sale=await saved();const other={...scope,branchId:new ObjectId()};await service.recover(db,other,'New');assert.equal(await db.collection('customers').countDocuments(),0);await service.recover(db,scope,'New');assert.equal(await db.collection('customers').countDocuments(),1); + await db.collection('customers').updateOne({_id:sale.customer_id},{$set:{is_deleted:true}});await assert.rejects(service.plan(db,scope,input),/deleted/);await service.finish(db,scope,sale._id);assert.equal((await db.collection('customers').findOne({_id:sale.customer_id})).is_deleted,true); +}); +test('cloud and other till reuse the same identifier and preserve references on round-trip',async()=>{ + const sale=await saved();await service.finish(db,scope,sale._id);const docs=await db.collection('customers').find().toArray();const cloud=client.db('sale_customer_cloud'),till=client.db('sale_customer_other');await cloud.dropDatabase();await till.dropDatabase();for(const target of [cloud,till]){for(const doc of docs)await target.collection('customers').replaceOne({_id:doc._id},doc,{upsert:true});assert.equal(String((await service.plan(target,scope,input)).id),String(sale.customer_id));}assert.notEqual(String(service.customerId({...scope,branchId:new ObjectId()},input.customer_name)),String(sale.customer_id));await cloud.dropDatabase();await till.dropDatabase(); +}); diff --git a/api/tests/unit/services/sale.service.test.js b/api/tests/unit/services/sale.service.test.js index fe88197ce..28acbcd63 100644 --- a/api/tests/unit/services/sale.service.test.js +++ b/api/tests/unit/services/sale.service.test.js @@ -194,7 +194,7 @@ describe('SalesService', () => { ['64f8f2f4c2b9c0a1e4b22222', ' tamil nadu ', '', false], ['64f8f2f4c2b9c0a1e4b22222', 'Karnataka', '', true], ['64f8f2f4c2b9c0a1e4b22222', '29', '', true], - ['64f8f2f4c2b9c0a1e4b22222', '29', '64f8f2f4c2b9c0a1e4b22222', false], + ['64f8f2f4c2b9c0a1e4b22222', '29', '64f8f2f4c2b9c0a1e4b22222', true], ])( 'saves GST by customer state (%s, %s, default %s)', async (customer_id, customer_state, default_customer, inter) => { @@ -223,7 +223,7 @@ describe('SalesService', () => { expect(saved.items[0].tax_components.map((component) => component.name)).toEqual( inter ? ['IGST'] : ['CGST', 'SGST'] ); - if (!customer_state || default_customer) expect(saved.customer_state).toBe('Tamil Nadu'); + if (!customer_state) expect(saved.customer_state).toBe('Tamil Nadu'); } ); @@ -330,6 +330,75 @@ describe('SalesService', () => { expect(mockCustomerRepositoryInstance.findById).not.toHaveBeenCalled(); }); + test('auto-create is finalized only after a successful sale write', async () => { + const helper = require('../../../src/services/sale-customer'); + const id = new (require('mongodb').ObjectId)(); + const plan = jest.spyOn(helper, 'plan').mockResolvedValue({ id, pending: true }); + const finish = jest.spyOn(helper, 'finish').mockResolvedValue(); + try { + mockItemRepositoryInstance.findItemById.mockResolvedValue(makeItemDoc()); + const result = await salesService.processSale( + makeSaleData({ customer_id: '', create_customer_on_save: true }), + '', + 'Add', + makeContext() + ); + expect(result.status).toBe(true); + expect(salesRepository.create.mock.calls[0][0].customer_creation_pending).toBe(true); + expect(String(salesRepository.create.mock.calls[0][0].customer_id)).toBe(String(id)); + expect(finish.mock.invocationCallOrder[0]).toBeGreaterThan( + salesRepository.create.mock.invocationCallOrder[0] + ); + } finally { + plan.mockRestore(); + finish.mockRestore(); + } + }); + test('a rejected sale never materializes the planned customer', async () => { + const helper = require('../../../src/services/sale-customer'); + const plan = jest + .spyOn(helper, 'plan') + .mockResolvedValue({ id: new (require('mongodb').ObjectId)(), pending: true }); + const finish = jest.spyOn(helper, 'finish').mockResolvedValue(); + try { + mockItemRepositoryInstance.findItemById.mockResolvedValue(null); + const result = await salesService.processSale( + makeSaleData({ customer_id: '', create_customer_on_save: true }), + '', + 'Add', + makeContext() + ); + expect(result.status).toBe(false); + expect(finish).not.toHaveBeenCalled(); + } finally { + plan.mockRestore(); + finish.mockRestore(); + } + }); + test('delivery state overrides customer state for GST without changing the customer snapshot', async () => { + mockItemRepositoryInstance.findItemById.mockResolvedValue( + makeItemDoc({ tax: 18, tax_type: 'exclusive' }) + ); + const result = await salesService.processSale( + makeSaleData({ + customer_state: 'Tamil Nadu', + place_of_supply: 'Karnataka', + sales_total: '236', + multi_payment: { Cash: 236 }, + }), + '', + 'Add', + makeContext({ + branchState: 'Tamil Nadu', + branchSettings: { sortname: 'IN', indian_gst: 'gst_on' }, + }) + ); + expect(result.status).toBe(true); + const saved = salesRepository.create.mock.calls[0][0]; + expect(saved.items[0].igst_tax).toBe(36); + expect(saved.customer_state).toBe('Tamil Nadu'); + expect(saved.place_of_supply).toBe('Karnataka'); + }); test('name-only customers cannot create a credit account implicitly', async () => { const result = await salesService.processSale( makeSaleData({ customer_id: '', customer_name: 'Visitor', partial_check: 'true' }), diff --git a/frontend/modules/sales_write.html b/frontend/modules/sales_write.html index 65875831d..c01acab34 100644 --- a/frontend/modules/sales_write.html +++ b/frontend/modules/sales_write.html @@ -136,7 +136,15 @@

- +
+ + + +
@@ -167,22 +175,22 @@

× - - -
+
-

Optional details for this bill. Use Save new customer to save these details as a customer record.

- - - - - + +
Address & delivery (optional) + + +
+ Walk-in customer @@ -470,7 +478,7 @@

Tax - + 0.00 + + 0.00 @@ -669,10 +677,8 @@
- - diff --git a/frontend/static/script/js/modules/js/sales.js b/frontend/static/script/js/modules/js/sales.js index a895999e8..fa05cb9ed 100644 --- a/frontend/static/script/js/modules/js/sales.js +++ b/frontend/static/script/js/modules/js/sales.js @@ -4348,10 +4348,12 @@ PosnicPro.sales.addSale = { redeem_points: (PosnicPro.loyalty ? PosnicPro.loyalty.redeemPointsForPayload() : 0), coupon_code: (PosnicPro.coupons ? PosnicPro.coupons.codeForPayload() : ''), customer_name: $('#sales_new_customer_name').val(), + create_customer_on_save: !$('#sales_new_customer_id').val() && !!$.trim($('#sales_new_customer_name').val()), customer_address: $('#sales_new_customer_address').val(), customer_phone: $('#sales_new_customer_phone').val(), customer_email: $('#sales_new_customer_email').val(), customer_state: $('#sales_new_customer_state').val(), + place_of_supply: $('#sales_place_of_supply').val(), customer_country: $('#sales_new_customer_country').val(), customer_gst_type: $('#sales_new_customer_gst_type').val(), customer_gst_number: $('#sales_new_customer_gst_number').val(), @@ -5013,11 +5015,13 @@ PosnicPro.sales.editSale = { sales_total: $("#grand_total").val(), customer_id: $('#sales_new_customer_id').val(), customer_name: ($('#sales_new_customer_name').val() === '') ? 'Anonymous' : $('#sales_new_customer_name').val(), + create_customer_on_save: !$('#sales_new_customer_id').val() && !!$.trim($('#sales_new_customer_name').val()), customer_address: $('#sales_new_customer_address').val(), sales_id: SalesDocumentId, customer_phone: $('#sales_new_customer_phone').val(), customer_email: $('#sales_new_customer_email').val(), customer_state: $('#sales_new_customer_state').val(), + place_of_supply: $('#sales_place_of_supply').val(), customer_country: $('#sales_new_customer_country').val(), customer_gst_type: $('#sales_new_customer_gst_type').val(), customer_gst_number: $('#sales_new_customer_gst_number').val(), @@ -5355,10 +5359,12 @@ PosnicPro.sales.holdSale = { sales_sub_total: String($('#sales_new_subtotal').text() || '').replace(/,/g, ''), customer_id: $('#sales_new_customer_id').val(), customer_name: $('#sales_new_customer_name').val(), + create_customer_on_save: !$('#sales_new_customer_id').val() && !!$.trim($('#sales_new_customer_name').val()), customer_address: $('#sales_new_customer_address').val(), customer_phone: $('#sales_new_customer_phone').val(), customer_email: $('#sales_new_customer_email').val(), customer_state: $('#sales_new_customer_state').val(), + place_of_supply: $('#sales_place_of_supply').val(), customer_country: $('#sales_new_customer_country').val(), customer_gst_type: $('#sales_new_customer_gst_type').val(), customer_gst_number: $('#sales_new_customer_gst_number').val(), @@ -7158,6 +7164,7 @@ PosnicPro.sales.calculation = { $('#sales_new_subtotal').number(addSalesLineSubTotal, 2); $("#sales_total_company_price").val(addSalesLineSubTotal); $("#tax").number(addSalesLineGstTaxTotal, 2); + if (PosnicPro.sales.updateCustomerGst) PosnicPro.sales.updateCustomerGst(addSalesLineGstTaxTotal); // Show line-item discounts plus any coupon/loyalty applied to the bill. var billDiscForShow = PosnicPro.sales.calculation.billLevelDiscount(); $("#discount_sale_amount").number(addReceivingLineDiscountTotal + billDiscForShow, 2); @@ -7413,7 +7420,7 @@ PosnicPro.sales.setSaleDefaults = function () { * own Walk-in is then resolved (the server finds or creates it), which * is exactly what the chip has been claiming all along. */ - $('#sales_new_customer_id,#sales_new_customer_name,#sales_new_customer_address,#sales_new_customer_phone,#sales_new_customer_email,#sales_new_customer_state,#sales_new_customer_gst_type,#sales_new_customer_gst_number,#sales_new_customer_partial_balance').val(''); + $('#sales_place_of_supply,#sales_new_customer_id,#sales_new_customer_name,#sales_new_customer_address,#sales_new_customer_phone,#sales_new_customer_email,#sales_new_customer_state,#sales_new_customer_gst_type,#sales_new_customer_gst_number,#sales_new_customer_partial_balance').val(''); PosnicPro.defaultcustomerSet(); if (PosnicPro.loyalty) PosnicPro.loyalty.tillClear(); if (PosnicPro.coupons) PosnicPro.coupons.tillClear(); @@ -7510,7 +7517,7 @@ PosnicPro.sales.setDefaults = function () { if (PosnicPro.sales.defaultCustomer === true) { // second copy of the reset above - same rule: clear the previous // customer, then resolve the branch Walk-in so the sale is billable - $('#sales_new_customer_id,#sales_new_customer_name,#sales_new_customer_address,#sales_new_customer_phone,#sales_new_customer_email,#sales_new_customer_state,#sales_new_customer_gst_type,#sales_new_customer_gst_number').val(''); + $('#sales_place_of_supply,#sales_new_customer_id,#sales_new_customer_name,#sales_new_customer_address,#sales_new_customer_phone,#sales_new_customer_email,#sales_new_customer_state,#sales_new_customer_gst_type,#sales_new_customer_gst_number').val(''); PosnicPro.defaultcustomerSet(); } PosnicPro.sales.SaleTableLineItems = []; @@ -8762,6 +8769,7 @@ PosnicPro.sales.recentMenu = { $('#sales_new_customer_phone').val(result.customer_phone || ''); $('#sales_new_customer_email').val(result.customer_email || ''); $('#sales_new_customer_state').val(result.customer_state || ''); + $('#sales_place_of_supply').val(result.place_of_supply || ''); if (PosnicPro.sales.updateCustomerChip) PosnicPro.sales.updateCustomerChip(); $('#sales_new_customer_country').val(result.customer_country || ''); $('#sales_new_customer_gst_type').val(result.customer_gst_type || ''); @@ -10997,7 +11005,7 @@ PosnicPro.sales.qr = { // A typed name is a sale snapshot, never a rename of the linked customer. $('#sales_new_customer_name').on('input', function () { if ($('#sales_new_customer_id').val()) { - $('#sales_new_customer_id,#sales_new_customer_phone,#sales_new_customer_email,#sales_new_customer_address,#sales_new_customer_state,#sales_new_customer_country,#sales_new_customer_gst_type,#sales_new_customer_gst_number,#sales_new_customer_partial_balance,#customer_current_balance').val(''); + $('#sales_place_of_supply,#sales_new_customer_id,#sales_new_customer_phone,#sales_new_customer_email,#sales_new_customer_address,#sales_new_customer_state,#sales_new_customer_country,#sales_new_customer_gst_type,#sales_new_customer_gst_number,#sales_new_customer_partial_balance,#customer_current_balance').val(''); PosnicPro.sales._customerCategoryId = ''; if (PosnicPro.loyalty && PosnicPro.loyalty.tillClear) PosnicPro.loyalty.tillClear(); if (PosnicPro.sales.refreshCustomerAccount) PosnicPro.sales.refreshCustomerAccount(); @@ -12035,6 +12043,7 @@ PosnicPro.sales.applyCustomerPick = function (data) { $('#sales_new_customer_phone').val(data.phone); $('#sales_new_customer_email').val(data.email); $('#sales_new_customer_state').val(data.state); + $('#sales_place_of_supply').val(''); $('#sales_new_customer_country').val(data.country); $('#sales_new_customer_gst_type').val(data.gst_type); $('#sales_new_customer_gst_number').val(data.gst_number); @@ -12057,6 +12066,8 @@ PosnicPro.sales.applyCustomerPick = function (data) { if (PosnicPro.loyalty) PosnicPro.loyalty.tillShow(data.id); if (PosnicPro.sales.updateCustomerChip) PosnicPro.sales.updateCustomerChip(); PosnicPro.sales.setCustomerPopup(false); + $('#sales_customer_search').prop('hidden', true); + if (PosnicPro.sales.updateCustomerGst) PosnicPro.sales.updateCustomerGst(); PosnicPro.sales._recentPush('recent_customers', { id: data.id, name: data.name, phone: data.phone, email: data.email, address: data.address, state: data.state, country: data.country, @@ -12234,6 +12245,7 @@ $(function () { PosnicPro.sales.applyCustomerPick(suggestion.data); } else { $('#sales_new_customer_name').val(suggestion.value.trim()).trigger('input'); + PosnicPro.sales.setCustomerPopup(true); } }, autoSelectFirst: false, @@ -12241,7 +12253,7 @@ $(function () { formatResult: function (suggestion) { let phone = suggestion.data.phone; if (suggestion.data === -1 || typeof suggestion.phone === undefined) { - phone = PosnicPro.i18n.t('lang_use_on_this_bill', 'Use on this bill'); + phone = 'New customer · saved with sale'; } let partial_balance_span = ""; if (suggestion.data.partial_balance === true) { @@ -12513,13 +12525,13 @@ PosnicPro.sales.updateCustomerChip = function () { if (id && !isDefaultCustomer) { $btn.addClass('has-customer'); - $icon.removeClass('icon-user').addClass('icon-user-check'); + $icon.removeClass('icon-user icon-user-check').addClass('icon-sliders'); var tip = (name || 'Customer') + (phone ? ' · ' + phone : ''); $btn.attr('data-original-title', tip).attr('title', tip).attr('aria-label', 'Customer: ' + tip); } else { $btn.removeClass('has-customer'); - $icon.removeClass('icon-user-check').addClass('icon-user'); + $icon.removeClass('icon-user icon-user-check').addClass('icon-sliders'); var t = name || PosnicPro.i18n.t('lang_customer_details', 'Customer details'); $btn.attr('data-original-title', t).attr('title', t).attr('aria-label', 'Customer: ' + t); } @@ -12541,12 +12553,66 @@ PosnicPro.sales.applySaleDateLock = function () { .attr('readonly', true).css({ 'pointer-events': 'none' }); } catch (e) { /* fail open: leave the date editable */ } }; +PosnicPro.sales.customerGstStates = []; +PosnicPro.sales.updateCustomerGst = function (tax) { + var enabled = PosnicPro.local.get('gst_action') === 'enable'; + $('.sales-customer-gst').prop('hidden', !enabled); + if (!enabled) { $('#sales_gst_breakdown').text(''); return; } + var states = PosnicPro.sales.customerGstStates; + $('#sales_customer_state_select').val($('#sales_new_customer_state').val() || ''); + $('#sales_place_of_supply_select').val($('#sales_place_of_supply').val() || ''); + function key(value) { + value = String(value || '').trim().toLowerCase(); + var state = states.find(function (s) { return s.value.toLowerCase() === value || s.id === value.padStart(2, '0'); }); + return state ? state.id : value; + } + var branch = PosnicPro.local.get('state_setting') || PosnicPro.local.get('statename') || ''; + var state = $('#sales_place_of_supply').val() || $('#sales_new_customer_state').val() || branch; + var interstate = !!(key(branch) && key(state) && key(branch) !== key(state)); + var amount = typeof tax === 'number' ? tax : Number($('#tax').text().replace(/,/g, '')) || 0; + var currency = PosnicPro.local.get('currencySign') || ''; + var gstin = ($('#sales_new_customer_gst_number').val() || '').trim().toUpperCase(); + var input = document.getElementById('sales_new_customer_gst_number'); + var error = gstin && !/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/.test(gstin) ? 'Enter a valid 15-character GSTIN.' : ''; + if (gstin.length === 15 && $('#sales_new_customer_state').val() && key($('#sales_new_customer_state').val()) !== gstin.slice(0, 2)) error = 'GSTIN and customer state do not match.'; + if (input) input.setCustomValidity(error); + $('#sales_customer_gst_hint').text(error || ('Place of supply: ' + (state || 'Select state') + ' · ' + (interstate ? 'IGST' : 'CGST + SGST'))); + $('#sales_gst_breakdown').text(amount > 0 ? (interstate ? 'IGST ' + currency + amount.toFixed(2) : 'CGST ' + currency + (amount / 2).toFixed(2) + ' + SGST ' + currency + (amount - Math.round(amount * 50) / 100).toFixed(2)) : ''); +}; +$('#sales_customer_state_select, #sales_place_of_supply_select, #sales_new_customer_gst_number').on('input change', function () { + if (this.id === 'sales_customer_state_select') $('#sales_new_customer_state').val(this.value); + if (this.id === 'sales_place_of_supply_select') $('#sales_place_of_supply').val(this.value); + $('#sales_new_customer_country').val('India'); + var gstin = ($('#sales_new_customer_gst_number').val() || '').trim().toUpperCase(); + $('#sales_new_customer_gst_type').val(gstin ? 'regular' : 'consumer'); + if (this.id === 'sales_new_customer_gst_number' && !$('#sales_new_customer_state').val()) { + var match = PosnicPro.sales.customerGstStates.find(function (state) { return state.id === gstin.slice(0, 2); }); + if (match) $('#sales_new_customer_state').val(match.value); + } + PosnicPro.sales.updateCustomerGst(); +}); +$(function () { + PosnicPro.sales.updateCustomerGst(); + if (PosnicPro.local.get('gst_action') !== 'enable') return; + PosnicPro.get({url:'setting/getJSONGstState'}, function (response) { + PosnicPro.sales.customerGstStates = response.data.gststate || []; + $('#sales_customer_state_select, #sales_place_of_supply_select').each(function () { + var current = this.value; + PosnicPro.sales.customerGstStates.forEach(function (state) { this.add(new Option(state.value + ' (' + state.id + ')', state.value)); }, this); + this.value = current; + }); + PosnicPro.sales.updateCustomerGst(); + }); +}); PosnicPro.sales.setCustomerPopup = function (open, returnFocus) { $('.toggle-customer-user').toggle(open); $('#sales_customer_btn').attr('aria-expanded', String(open)); if (open) { - PosnicPro.sales.renderRecentCustomers(); - $('#sales_new_customer_name').trigger('focus').trigger('select'); + $('#sales_customer_search').prop('hidden', true); + $('#sales_customer_detail_name').text($('#sales_new_customer_name').val() || 'Walk-in customer'); + $('#sales_customer_status').text($('#sales_new_customer_id').val() ? '' : 'New customer will be saved with the sale.'); + PosnicPro.sales.updateCustomerGst(); + $('#sales_new_customer_phone').trigger('focus'); } else { $('#sales_customer_results .autocomplete-suggestions').hide(); if (returnFocus) $('#sales_customer_btn').trigger('focus'); @@ -12566,36 +12632,22 @@ $(document).on('keydown', '#sales_customer_popup', function (e) { } }); $(document).on('click', '#sales_customer_add_new', function () { - var $button = $(this); - if ($button.prop('disabled')) return; - var data = {}; - ['name', 'phone', 'email', 'address'].forEach(function (key) { - data[key] = ($('#sales_new_customer_' + key).val() || '').trim(); - }); - var $status = $('#sales_customer_status'); - if (!data.name || $('#sales_new_customer_id').val()) { - $status.text(PosnicPro.i18n.t('lang_enter_a_new_customer_name_first_or_select', 'Enter a new customer name first, or select an existing customer.')); - $('#sales_new_customer_name').trigger('focus'); - return; - } - $button.prop('disabled', true); - $status.text(PosnicPro.i18n.t('lang_saving', 'Saving ...')); - PosnicPro.request({method: 'POST', url: 'customers', data: JSON.stringify(data)}, function (response) { - $button.prop('disabled', false); - var saved = response.data || {}; - var id = saved.id || saved.customer_id || saved._id; - if (response.type !== 'success' || !id) { - $status.text(response.message || 'Could not save customer. Please check the details.'); - return; - } - $status.text(PosnicPro.i18n.t('lang_customer_saved', 'Customer saved.')); - PosnicPro.sales.applyCustomerPick(Object.assign({}, data, saved, {id: id})); - PosnicPro.sales.setCustomerPopup(false, true); - }, function (xhr) { - $button.prop('disabled', false); - var response = xhr.responseJSON || {}; - $status.text(response.message || 'Could not save customer. Check the details and try again.'); - }); + PosnicPro.sales.setCustomerPopup(true); +}); +$('#sales_new_customer_name').on('focus input', function () { + $('.toggle-customer-user').hide(); + $('#sales_customer_btn').attr('aria-expanded', 'false'); + $('#sales_customer_search').prop('hidden', false); + PosnicPro.sales.renderRecentCustomers(); + $('#sales_recent_customers').toggle(!this.value.trim()); + $('#sales_customer_add_new').text(this.value.trim() ? 'Create ' + this.value.trim() + ' · add details' : 'Add customer details'); +}); +$('#sales_new_customer_name').on('keydown', function (e) { + if (e.key === 'Escape') $('#sales_customer_search').prop('hidden', true); +}); +$(document).on('mousedown', function (e) { + if (!$(e.target).closest('.sales-customer-entry, .autocomplete-suggestions').length) + $('#sales_customer_search').prop('hidden', true); }); // Customer popover: close on the × or an outside click; "Walk-in" clears the // customer back to none. Selecting a search result (autocomplete onSelect) also @@ -12606,7 +12658,7 @@ $(document).on('click', '#sales_customer_pop_close', function () { }); $(document).on('click', '#sales_customer_walkin', function (e) { e.preventDefault(); - $('#sales_new_customer_id,#sales_new_customer_name,#sales_new_customer_address,#sales_new_customer_phone,#sales_new_customer_email,#sales_new_customer_state,#sales_new_customer_gst_type,#sales_new_customer_gst_number,#sales_new_customer_partial_balance').val(''); + $('#sales_place_of_supply,#sales_new_customer_id,#sales_new_customer_name,#sales_new_customer_address,#sales_new_customer_phone,#sales_new_customer_email,#sales_new_customer_state,#sales_new_customer_gst_type,#sales_new_customer_gst_number,#sales_new_customer_partial_balance').val(''); /* * Walk-in means the branch's OWN Walk-in customer record - the same one * a fresh sale starts with - not "no customer at all". Blanking every diff --git a/frontend/static/style/css/theme-variables.css b/frontend/static/style/css/theme-variables.css index 5f2d0c566..c4ccea4cb 100644 --- a/frontend/static/style/css/theme-variables.css +++ b/frontend/static/style/css/theme-variables.css @@ -3580,3 +3580,16 @@ html:not([data-theme]) .loadingSpinner::after { #sales_customer_results { position: relative; } #sales_customer_results .autocomplete-suggestions { position: relative !important; top: auto !important; left: auto !important; width: 100% !important; max-height: 180px !important; } #sales_customer_add_new { background: transparent; border: 1px solid currentColor; margin: 10px 0; } + +/* Direct customer entry alongside item search; optional details stay compact. */ +.sales-search-toolbar .sales-customer-entry { flex:0 1 38%; min-width:180px; margin:0; gap:0; flex-wrap:nowrap; position:relative; } +.sales-search-toolbar .sales-customer-entry input { width:0; flex:1 1 0; border-radius:5px 0 0 5px; } +.sales-search-toolbar .sales-customer-btn { height:38px; border-radius:0 5px 5px 0; } +.sales-customer-search-pop { position:absolute; top:44px; right:0; width:100%; min-width:260px; z-index:1051; padding:8px; background:var(--theme-body-bg); color:var(--theme-text-primary); border:1px solid var(--theme-border-color); border-radius:8px; box-shadow:0 8px 24px #0003; } +#sales_customer_popup { width:360px; padding:16px; } +#sales_customer_popup .sales-customer-address { margin:16px 0; } +#sales_customer_popup .sales-customer-pop-action { display:inline-flex; } +#sales_customer_add_new { width:100%; text-align:left; border:0; border-top:1px solid var(--theme-border-color); margin:4px 0 0; border-radius:0; } +.sales-customer-gst[hidden],#sales_customer_search[hidden] { display:none!important; } +#sales_gst_breakdown { color:var(--theme-text-primary); margin-top:4px; } +@media(max-width:480px) { .sales-search-toolbar { flex-wrap:wrap; } .sales-search-toolbar #sales_new_item_name { flex-basis:100%; } .sales-search-toolbar .sales-customer-entry { flex-basis:100%; } } diff --git a/tests/sales-customer-gst.test.js b/tests/sales-customer-gst.test.js new file mode 100644 index 000000000..ef0685eec --- /dev/null +++ b/tests/sales-customer-gst.test.js @@ -0,0 +1,10 @@ +const test=require('node:test'),assert=require('node:assert/strict'),fs=require('fs');const {JSDOM}=require('jsdom'),jquery=require('jquery'); +test('state selection previews IGST, local split, delivery override and zero-tax exemption',()=>{ + const dom=new JSDOM(fs.readFileSync('frontend/modules/sales_write.html','utf8'),{runScripts:'outside-only'});const w=dom.window,$=jquery(w);w.$=$;w.PosnicPro={sales:{},local:{get:k=>({gst_action:'enable',state_setting:'Goa',currencySign:'₹'})[k]}}; + try {const source=fs.readFileSync('frontend/static/script/js/modules/js/sales.js','utf8');const a=source.indexOf('PosnicPro.sales.customerGstStates =');const b=source.indexOf("$('#sales_customer_state_select, #sales_place_of_supply_select, #sales_new_customer_gst_number').on",a);w.eval(source.slice(a,b));w.PosnicPro.sales.customerGstStates=[{id:'30',value:'Goa'},{id:'29',value:'Karnataka'}]; + $('#sales_new_customer_state').val('Karnataka');w.PosnicPro.sales.updateCustomerGst(18);assert.match($('#sales_gst_breakdown').text(),/IGST ₹18.00/); + $('#sales_place_of_supply').val('Goa');w.PosnicPro.sales.updateCustomerGst(18);assert.match($('#sales_gst_breakdown').text(),/CGST ₹9.00 \+ SGST ₹9.00/); + w.PosnicPro.sales.updateCustomerGst(0);assert.equal($('#sales_gst_breakdown').text(),''); + $('#sales_new_customer_gst_number').val('30ABCDE1234F1Z5');w.PosnicPro.sales.updateCustomerGst();assert.match(w.document.getElementById('sales_new_customer_gst_number').validationMessage,/do not match/); + }finally{dom.window.close()} +}); diff --git a/tests/sales-customer-popup.test.js b/tests/sales-customer-popup.test.js index 133524cb5..c37ad4061 100644 --- a/tests/sales-customer-popup.test.js +++ b/tests/sales-customer-popup.test.js @@ -13,29 +13,28 @@ function setup(){ w.eval(source.slice(a,source.indexOf('// Customer popover:',a))); return {dom,w,$,calls}; } -test('customer icon shares the search row and opens one popup with focus and Escape dismissal',()=>{ +test('customer name stays beside item search and details opens separately',()=>{ const {dom,w,$}=setup();try{ - assert.equal($('#sales_customer_btn').closest('.sales-search-toolbar').find('#sales_new_item_name').length,1); - assert.equal($('#sales_customer_popup #sales_new_customer_name').length,1); - $('#sales_customer_btn').trigger('click');assert.equal($('#sales_customer_btn').attr('aria-expanded'),'true'); - assert.equal(w.document.activeElement.id,'sales_new_customer_name'); - $('#sales_new_customer_name').trigger($.Event('keydown',{key:'Escape'})); - assert.equal($('#sales_customer_btn').attr('aria-expanded'),'false');assert.equal(w.document.activeElement.id,'sales_customer_btn'); + w.PosnicPro.sales.updateCustomerGst=()=>{}; + assert.equal($('#sales_new_customer_name').closest('.sales-search-toolbar').length,1); + assert.equal($('#sales_customer_popup #sales_new_customer_name').length,0); + $('#sales_new_customer_name').val('Prasad').trigger('input'); + assert.equal($('#sales_customer_search').prop('hidden'),false); + $('#sales_customer_btn').trigger('click'); + assert.equal($('#sales_customer_btn').attr('aria-expanded'),'true'); + assert.equal($('#sales_customer_search').prop('hidden'),true); + assert.equal(w.document.activeElement.id,'sales_new_customer_phone'); + $('#sales_new_customer_phone').trigger($.Event('keydown',{key:'Escape'})); + assert.equal($('#sales_customer_btn').attr('aria-expanded'),'false'); }finally{dom.window.close();} }); -test('inline customer creation prevents double submission, reports failures, and selects successful record',()=>{ - const {dom,$,calls}=setup();try{ - $('#sales_new_customer_name').val('New customer');$('#sales_new_customer_phone').val('9876543210'); - $('#sales_customer_add_new').trigger('click').trigger('click');assert.equal(calls.length,1); - assert.equal(calls[0].p.url,'customers');assert.equal(JSON.parse(calls[0].p.data).name,'New customer'); - calls[0].fail({responseJSON:{message:'Phone already exists'}});assert.match($('#sales_customer_status').text(),/Phone already exists/); - assert.equal($('#sales_customer_add_new').prop('disabled'),false); - $('#sales_customer_add_new').trigger('click');calls[1].ok({type:'success',data:{customer_id:'abc',name:'New customer'}}); - assert.equal(calls[2].id,'abc');assert.equal($('#sales_customer_btn').attr('aria-expanded'),'false'); - }finally{dom.window.close();} -}); -test('saving an already linked customer cannot create a duplicate',()=>{ - const {dom,$,calls}=setup();try{ - $('#sales_new_customer_name').val('Existing');$('#sales_new_customer_id').val('abc');$('#sales_customer_add_new').trigger('click');assert.equal(calls.length,0); +test('entering optional new customer details performs no write before sale save',()=>{ + const {dom,w,$,calls}=setup();try{ + w.PosnicPro.sales.updateCustomerGst=()=>{}; + $('#sales_new_customer_name').val('New customer');$('#sales_customer_add_new').trigger('click'); + $('#sales_new_customer_phone').val('9876543210');$('#sales_customer_done').trigger('click'); + assert.equal(calls.length,0);assert.equal($('#sales_new_customer_name').val(),'New customer'); + assert.equal($('#sales_new_customer_phone').val(),'9876543210'); + assert.equal($('#sales_customer_btn').attr('aria-expanded'),'false'); }finally{dom.window.close();} }); From bf86f03d423f5455cde5b71836a83d7a4fbc74c2 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Fri, 16 Oct 2026 14:45:46 +0530 Subject: [PATCH 2/2] Polish record editors and allow explicit shared customer phones --- api/src/controllers/customers.controller.js | 4 ++ .../migrations/supplier-email-index.js | 15 ++++--- api/src/models/customer.model.js | 2 +- api/src/repositories/customer.repository.js | 3 +- api/src/services/customer.service.js | 10 +++-- .../controllers/customers.controller.test.js | 4 ++ api/tests/unit/models/customer.model.test.js | 5 ++- .../repositories/customer.repository.test.js | 11 +++++ .../unit/services/customer.service.test.js | 43 ++++++++++++++++++- docs/ORGANIZED_RECORD_FORMS.md | 8 ++++ frontend/modals/customer.html | 3 +- frontend/modals/user.html | 4 +- frontend/modules/sales_write.html | 18 ++++---- .../static/script/js/core/record-workspace.js | 16 ++++++- .../static/script/js/modules/js/customers.js | 2 + frontend/static/script/js/modules/js/users.js | 13 +++++- .../js/validpassword-showhidepassword.js | 4 +- .../static/style/css/record-workspace.css | 35 ++++++++++++--- languages/_english.json | 18 ++++---- languages/ar.json | 8 +++- languages/bg.json | 8 +++- languages/bn.json | 8 +++- languages/bs.json | 8 +++- languages/cs.json | 8 +++- languages/da.json | 8 +++- languages/de.json | 8 +++- languages/el.json | 8 +++- languages/es.json | 8 +++- languages/et.json | 8 +++- languages/fa.json | 8 +++- languages/fi.json | 8 +++- languages/fr.json | 8 +++- languages/ga.json | 8 +++- languages/gu.json | 8 +++- languages/he.json | 8 +++- languages/hi.json | 8 +++- languages/hr.json | 8 +++- languages/hu.json | 8 +++- languages/id.json | 8 +++- languages/is.json | 8 +++- languages/it.json | 8 +++- languages/ja.json | 8 +++- languages/kn.json | 8 +++- languages/ko.json | 8 +++- languages/lt.json | 8 +++- languages/lv.json | 8 +++- languages/mk.json | 8 +++- languages/ml.json | 8 +++- languages/mr.json | 8 +++- languages/ms.json | 8 +++- languages/mt.json | 8 +++- languages/nb.json | 8 +++- languages/ne.json | 12 +++++- languages/nl.json | 12 +++++- languages/pa.json | 8 +++- languages/pl.json | 8 +++- languages/pt.json | 8 +++- languages/ro.json | 8 +++- languages/ru.json | 8 +++- languages/si.json | 8 +++- languages/sk.json | 8 +++- languages/sl.json | 8 +++- languages/sq.json | 8 +++- languages/sr.json | 8 +++- languages/sv.json | 8 +++- languages/sw.json | 8 +++- languages/ta.json | 8 +++- languages/te.json | 8 +++- languages/th.json | 8 +++- languages/tl.json | 8 +++- languages/tr.json | 8 +++- languages/uk.json | 8 +++- languages/ur.json | 8 +++- languages/vi.json | 8 +++- languages/zh-CN.json | 8 +++- languages/zh-TW.json | 8 +++- tests/cloud-agent-bootstrap.test.js | 4 +- tests/record-workspace.test.js | 22 +++++++++- 78 files changed, 603 insertions(+), 105 deletions(-) diff --git a/api/src/controllers/customers.controller.js b/api/src/controllers/customers.controller.js index d1f217ce5..78657cf7e 100644 --- a/api/src/controllers/customers.controller.js +++ b/api/src/controllers/customers.controller.js @@ -190,6 +190,7 @@ class CustomerController extends BaseController { name: req.body.name, email: req.body.email || '', phone: req.body.phone || '', + allow_shared_phone: normalizeBoolean(req.body.allow_shared_phone), alternatePhone: req.body.alternatePhone, address: req.body.address || '', city: req.body.city || '', @@ -310,6 +311,9 @@ class CustomerController extends BaseController { // Extract update data const updateData = { ...req.body }; + if (req.body.allow_shared_phone !== undefined) { + updateData.allow_shared_phone = normalizeBoolean(req.body.allow_shared_phone); + } // Convert branch_id to ObjectId if present if (updateData.branch_id) { diff --git a/api/src/database/migrations/supplier-email-index.js b/api/src/database/migrations/supplier-email-index.js index 85e55c702..1df9fe678 100644 --- a/api/src/database/migrations/supplier-email-index.js +++ b/api/src/database/migrations/supplier-email-index.js @@ -40,15 +40,16 @@ const supplierKeys = { license: 1, branch_id: 1, email: 1 }; const supplierOptions = { ...options, name: 'supplier_branch_nonempty_email' }; const migrateSupplierEmailIndex = (db) => - migrateContactEmailIndex( - db, - 'suppliers', - supplierOptions, - supplierKeys - ); + migrateContactEmailIndex(db, 'suppliers', supplierOptions, supplierKeys); const migrateCustomerEmailIndex = (db) => migrateContactEmailIndex(db, 'customers', { ...options, name: 'customer_nonempty_email', }); -module.exports = { migrateSupplierEmailIndex, migrateCustomerEmailIndex, options, supplierKeys, supplierOptions }; +module.exports = { + migrateSupplierEmailIndex, + migrateCustomerEmailIndex, + options, + supplierKeys, + supplierOptions, +}; diff --git a/api/src/models/customer.model.js b/api/src/models/customer.model.js index 9c91b69e2..b1189ebd2 100644 --- a/api/src/models/customer.model.js +++ b/api/src/models/customer.model.js @@ -41,8 +41,8 @@ class CustomerModel extends BaseModel { type: 'String', select: true, required: true, - unique: true, }, + allow_shared_phone: { type: 'Boolean', select: true, default: false }, alternatePhone: { type: 'String', select: true }, // Address Information diff --git a/api/src/repositories/customer.repository.js b/api/src/repositories/customer.repository.js index 65438fd47..29d9b70bd 100644 --- a/api/src/repositories/customer.repository.js +++ b/api/src/repositories/customer.repository.js @@ -85,10 +85,11 @@ class CustomerRepository extends BaseModel { /** * Find customer by phone */ - async findByPhone(phone) { + async findByPhone(phone, excludeId) { const collection = await this.getCollection(this.collectionName); return await collection.findOne({ phone, + ...(excludeId ? { _id: { $ne: new ObjectId(excludeId) } } : {}), license: BaseModel.license, is_deleted: { $ne: true }, }); diff --git a/api/src/services/customer.service.js b/api/src/services/customer.service.js index 5a30b297c..1fd2e31b3 100644 --- a/api/src/services/customer.service.js +++ b/api/src/services/customer.service.js @@ -230,7 +230,7 @@ class CustomerService { } // Check for duplicate phone (only if phone is provided) - if (customerData.phone) { + if (customerData.phone && customerData.allow_shared_phone !== true) { const existingByPhone = await this.repository.findByPhone(customerData.phone); if (existingByPhone) { return { @@ -296,8 +296,12 @@ class CustomerService { } // Check for duplicate phone if phone is being updated - if (updateData.phone && updateData.phone !== existing.phone) { - const existingByPhone = await this.repository.findByPhone(updateData.phone); + const sharedPhone = updateData.allow_shared_phone ?? existing.allow_shared_phone; + const phone = updateData.phone ?? existing.phone; + const revokedSharing = + updateData.allow_shared_phone === false && existing.allow_shared_phone === true; + if (phone && (phone !== existing.phone || revokedSharing) && sharedPhone !== true) { + const existingByPhone = await this.repository.findByPhone(phone, id); if (existingByPhone && existingByPhone._id.toString() !== id) { return { status: false, diff --git a/api/tests/unit/controllers/customers.controller.test.js b/api/tests/unit/controllers/customers.controller.test.js index 361619f14..11607d87d 100644 --- a/api/tests/unit/controllers/customers.controller.test.js +++ b/api/tests/unit/controllers/customers.controller.test.js @@ -23,6 +23,9 @@ jest.mock('express-async-handler', () => (fn) => fn); jest.mock('../../../src/services/customer.service'); +jest.mock('../../../src/services/sale-customer', () => ({ + recover: jest.fn().mockResolvedValue(), +})); jest.mock('express-validator', () => ({ validationResult: jest.fn() })); // base.service transitive mock @@ -44,6 +47,7 @@ jest.mock('../../../src/models/base.model', () => { static license = null; static loggedUser = null; static loggedUserName = ''; + static getDb = jest.fn().mockResolvedValue({}); } MockBaseModel.prototype.getCollection = mockGetCollection; return MockBaseModel; diff --git a/api/tests/unit/models/customer.model.test.js b/api/tests/unit/models/customer.model.test.js index b3d644d9f..dc091c517 100644 --- a/api/tests/unit/models/customer.model.test.js +++ b/api/tests/unit/models/customer.model.test.js @@ -100,8 +100,9 @@ describe('CustomerModel — this.fields: unique constraints', () => { expect(cm.fields.email.unique).toBe(true); }); - test('phone has unique:true', () => { - expect(cm.fields.phone.unique).toBe(true); + test('shared phone numbers are controlled by explicit opt-in, not a global unique constraint', () => { + expect(cm.fields.phone.unique).not.toBe(true); + expect(cm.fields.allow_shared_phone.default).toBe(false); }); }); diff --git a/api/tests/unit/repositories/customer.repository.test.js b/api/tests/unit/repositories/customer.repository.test.js index 38a34c3a8..988de4e6c 100644 --- a/api/tests/unit/repositories/customer.repository.test.js +++ b/api/tests/unit/repositories/customer.repository.test.js @@ -318,6 +318,17 @@ describe('CustomerRepository (class, extends BaseModel)', () => { // ── findByPhone ──────────────────────────────────────────────────────────── describe('findByPhone', () => { + test('excludes the edited customer while preserving tenant scope', async () => { + await repository.findByPhone('9876543210', '507f1f77bcf86cd799439011'); + expect(mockCollection.findOne).toHaveBeenCalledWith( + expect.objectContaining({ + phone: '9876543210', + _id: { $ne: expect.anything() }, + license: BaseModel.license, + }) + ); + }); + test('calls collection.findOne with phone', async () => { await repository.findByPhone('9876543210'); expect(mockCollection.findOne).toHaveBeenCalledWith( diff --git a/api/tests/unit/services/customer.service.test.js b/api/tests/unit/services/customer.service.test.js index 6f2a6e315..0e043b50a 100644 --- a/api/tests/unit/services/customer.service.test.js +++ b/api/tests/unit/services/customer.service.test.js @@ -490,6 +490,21 @@ describe('CustomerService', () => { }); }); + test('an explicit shared-phone opt-in permits a separate customer identity', async () => { + repo.findByPhone.mockResolvedValue(makeMockCustomer()); + repo.create.mockResolvedValue(makeMockCustomer()); + const result = await service.createCustomer({ + ...validPayload(), + phone: '9876543210', + allow_shared_phone: true, + }); + expect(result.status).toBe(true); + expect(repo.findByPhone).not.toHaveBeenCalled(); + expect(repo.create).toHaveBeenCalledWith( + expect.objectContaining({ allow_shared_phone: true }) + ); + }); + test('initializes loyalty when enableLoyalty is true', async () => { repo.create.mockResolvedValue(makeMockCustomer()); const payload = { ...validPayload(), enableLoyalty: true }; @@ -612,7 +627,7 @@ describe('CustomerService', () => { await service.updateCustomer(CUSTOMER_ID, { phone: '222' }); - expect(repo.findByPhone).toHaveBeenCalledWith('222'); + expect(repo.findByPhone).toHaveBeenCalledWith('222', CUSTOMER_ID); }); test('returns duplicate phone error when another customer has new phone', async () => { @@ -631,6 +646,32 @@ describe('CustomerService', () => { }); }); + test('an existing shared-phone opt-in survives edits that omit the preference', async () => { + repo.findById.mockResolvedValue({ ...existing(), allow_shared_phone: true }); + repo.update.mockResolvedValue({}); + const result = await service.updateCustomer(CUSTOMER_ID, { phone: '222' }); + expect(result.status).toBe(true); + expect(repo.findByPhone).not.toHaveBeenCalled(); + }); + + test('turning shared-phone permission off restores duplicate checks on a changed phone', async () => { + repo.findById.mockResolvedValue({ ...existing(), allow_shared_phone: true }); + repo.findByPhone.mockResolvedValue({ _id: { toString: () => OTHER_ID } }); + const result = await service.updateCustomer(CUSTOMER_ID, { + phone: '222', + allow_shared_phone: false, + }); + expect(result.status).toBe(false); + }); + + test('revoking sharing checks an unchanged phone against other customers', async () => { + repo.findById.mockResolvedValue({ ...existing(), allow_shared_phone: true }); + repo.findByPhone.mockResolvedValue({ _id: { toString: () => OTHER_ID } }); + const result = await service.updateCustomer(CUSTOMER_ID, { allow_shared_phone: false }); + expect(result.status).toBe(false); + expect(repo.findByPhone).toHaveBeenCalledWith(existing().phone, CUSTOMER_ID); + }); + test('returns {status:true, data:updatedCustomer} on success', async () => { const updated = makeMockCustomer({ name: 'Updated' }); repo.findById.mockResolvedValueOnce(existing()).mockResolvedValueOnce(updated); diff --git a/docs/ORGANIZED_RECORD_FORMS.md b/docs/ORGANIZED_RECORD_FORMS.md index cb61f678b..69d661a15 100644 --- a/docs/ORGANIZED_RECORD_FORMS.md +++ b/docs/ORGANIZED_RECORD_FORMS.md @@ -20,3 +20,11 @@ New customers, suppliers and branches start with the current shop's country, sta Ship the API city persistence change and frontend assets together. Electron menu organization needs the next desktop installer; the web form layout does not. Preserve independent recovery windows. Do not publish the desktop latest channel before the requested private installer test. Local verification includes actual-template DOM regression tests for control ownership and values, tab validation, state-selection races, existing phone/permission/settings tests, API branch creation, translation coverage, frontend build, and isolated Electron-rendered layout previews. These previews do not save live shop records. + +## Form polish and shared customer phones + +Editors now size to their content, with a viewport height limit and scrolling for long forms. Small record editors use a 560px maximum width; user, customer, supplier, branch and product-category editors retain their wider layout. The branch edit page fills its available content area. Account choices are labelled Staff account and API integration; legacy normal roles display as Staff. Email validation accepts plus addresses and longer top-level domains. Required phone/state validation remains in place. + +Customer phone sharing is an explicit per-record opt-in, unchecked for new records. The API normalizes and persists customers.allow_shared_phone; omitted values on edits preserve the saved preference. Duplicate prevention remains the default. Revoking sharing checks for another customer with the same phone, excluding the edited record by ID. Emails and record IDs retain their existing behavior. The live target shop has no unique phone index, so no index removal or data migration is required. + +The new boolean is shared business data in the existing branch-scoped customers document and batch lane in the POS, Gateway and agent manifests. Whole-document replication retains the field; IDs, branch access, timestamps, conflict handling and tombstones remain unchanged. On reconnect, the saved preference must travel with the customer to cloud and other authorized tills. No new collections or Gateway/agent configuration are needed. Deploy frontend and API together; desktop offline use needs the updated bundled API. Live cross-device create/update/delete, retry and reconnection were not exercised, and no desktop installer was published for this polish. diff --git a/frontend/modals/customer.html b/frontend/modals/customer.html index 82b57da8d..c7b016439 100644 --- a/frontend/modals/customer.html +++ b/frontend/modals/customer.html @@ -28,6 +28,7 @@

+
@@ -893,4 +894,4 @@

- \ No newline at end of file + diff --git a/frontend/modals/user.html b/frontend/modals/user.html index 894ecd45a..39f0043d9 100644 --- a/frontend/modals/user.html +++ b/frontend/modals/user.html @@ -47,7 +47,7 @@

' + PosnicPro.listDoc.grid([ @@ -296,7 +305,7 @@ PosnicPro.users = { + '' + '' + esc(r.username) + '' + '' + esc(r.email || '-') + '' - + '' + esc(r.usertype) + '' + + '' + esc(PosnicPro.users.roleLabel(r)) + '' + '' + status + '' + ''; }); diff --git a/frontend/static/script/js/validpassword-showhidepassword.js b/frontend/static/script/js/validpassword-showhidepassword.js index c77b28d61..501da6960 100644 --- a/frontend/static/script/js/validpassword-showhidepassword.js +++ b/frontend/static/script/js/validpassword-showhidepassword.js @@ -21,7 +21,7 @@ $.validator.addMethod("strong_password", function (value, element) { }); jQuery.validator.addMethod("emailExt", function (value, element, param) { - return value.match(/^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/); + return this.optional(element) || /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value); }, 'please enter a valid email'); jQuery.validator.addMethod("city", function (city_name, element) { @@ -68,4 +68,4 @@ $(".current-show_hide_password").on('click', function (event) { $('#old_password,#password').attr('type', 'text'); $('.current-show_hide_password i').removeClass("fa-eye-slash").addClass("fa-eye"); } -}); \ No newline at end of file +}); diff --git a/frontend/static/style/css/record-workspace.css b/frontend/static/style/css/record-workspace.css index 7484ff75b..9ac246e2a 100644 --- a/frontend/static/style/css/record-workspace.css +++ b/frontend/static/style/css/record-workspace.css @@ -2,7 +2,9 @@ .record-dialog.sidebarshow { width: min(820px, calc(100vw - 64px)) !important; max-width: none !important; - height: calc(100dvh - 64px); + height: auto; + max-height: calc(100dvh - 64px); + bottom: auto; top: 32px; right: max(32px, calc((100vw - 820px) / 2)); border: 1px solid var(--theme-border-color); @@ -21,7 +23,7 @@ border-bottom: 1px solid var(--theme-border-color); } .record-dialog .infobar-settings-sidebar-head h4 { color: var(--theme-text-primary); font-weight: 600; } -.record-dialog .infobar-settings-sidebar-body { padding-bottom: 0; } +.record-dialog .infobar-settings-sidebar-body { padding-bottom: 0; background: var(--theme-card-bg); } .record-dialog .custom-mode-setting { padding: 24px 28px 0 !important; } .record-dialog .profilebox { padding: 0 0 16px; text-align: left !important; } .record-dialog .profilebox img { width: 64px !important; height: 64px !important; object-fit: contain; } @@ -40,7 +42,7 @@ .record-dialog .select2-selection { min-height: 44px; border-radius: 8px; } .record-dialog .form-control::placeholder { color: var(--theme-text-muted) !important; opacity: 1; } .record-form-panel[hidden] { display: none !important; } -.record-dialog .record-form-panel { min-height: 250px; } +.record-dialog .record-form-panel { min-height: 0; } .record-dialog label { color: var(--theme-text-primary); } .record-dialog .accordion .btn-link { color: var(--theme-text-primary) !important; } .record-dialog .form-group { margin-bottom: 20px; } @@ -73,7 +75,7 @@ .record-sales-panel > .form-group { padding: 16px; border-bottom: 1px solid var(--theme-border-color); margin: 0; } .record-sales-panel fieldset { width: 100%; background: var(--theme-card-bg); box-shadow: none; } .record-sales-panel > fieldset label { padding: 6px 0; } -.record-branch-page .contentbar { max-width: 1200px; margin: auto; } +.record-branch-page .contentbar { width: 100%; max-width: none; margin: 0; } .record-branch-page .record-section { background: var(--theme-card-bg); border: 1px solid var(--theme-border-color); border-radius: 12px; padding: 24px; } .record-branch-page .profilebox img { max-width: 80px; max-height: 80px; } .record-branch-page .record-details { padding: 20px 24px; background: var(--theme-card-bg); margin-bottom: 20px; border-radius: 12px; } @@ -89,7 +91,7 @@ #category_tile_swatches .tile-swatch.is-picked { outline: 2px solid var(--theme-primary-color); outline-offset: 2px; } [dir="rtl"] .record-dialog.sidebarshow { left: max(32px, calc((100vw - 820px) / 2)); right: auto; } @media (max-width: 767.98px) { - .record-dialog.sidebarshow { width: 100vw !important; height: 100dvh; top: 0; right: 0; border-radius: 0; } + .record-dialog.sidebarshow { width: 100vw !important; height: auto; max-height: 100dvh; top: 0; right: 0; border-radius: 0; } [dir="rtl"] .record-dialog.sidebarshow { left: 0; right: auto; } .record-dialog .custom-mode-setting { padding: 20px 18px 0 !important; } .record-dialog .infobar-settings-sidebar-head { padding: 14px 18px; } @@ -98,3 +100,26 @@ .record-tabs button { padding: 12px; } } @media (prefers-reduced-motion: reduce) { .record-dialog { transition: none !important; } } +.record-dialog-small.sidebarshow { width: min(560px, calc(100vw - 48px)) !important; right: max(24px, calc((100vw - 560px) / 2)); } +[dir="rtl"] .record-dialog-small.sidebarshow { left: max(24px, calc((100vw - 560px) / 2)); right: auto; } +.record-dialog .card-footer { background: var(--theme-card-bg); border: 0; padding: 0; } +.record-dialog .custom-mode-setting { background: var(--theme-card-bg); } +.record-account-types { margin: 0 0 20px !important; } +.record-account-types > div > .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.record-account-types .col-md-6 { width: 100%; max-width: none; padding: 12px 16px; text-align: left !important; border: 1px solid var(--theme-border-color); border-radius: 10px; } +.record-account-types .col-md-6:has(input:checked) { border-color: var(--theme-primary-color); background: var(--theme-body-bg); } +.record-account-types label { margin-bottom: 0; cursor: pointer; } +.record-account-types .example { margin: 0; } +.record-account-status { margin: 16px 0 0 !important; padding: 0; } +.record-account-status > .row { margin: 0; gap: 24px; } +.record-account-status .col-md-6 { flex: none; width: auto; max-width: none; padding: 0; text-align: left !important; } +#user_form_upload:has(#apiMethod:checked) .record-staff-profile, +#user_form_upload:has(#apiMethod:checked) .profilebox { display: none; } +#units_doc:has(#unit_page_name) { max-width: 680px; margin-inline: auto; } +#units_doc:has(#unit_page_name) .s-doc-body { padding: 16px; } +#units_doc:has(#unit_page_name) .q-sheet { min-height: 0; } +@media (max-width: 767.98px) { + .record-dialog-small.sidebarshow { width: 100vw !important; right: 0; } + [dir="rtl"] .record-dialog-small.sidebarshow { left: 0; } + .record-account-types > div > .row { grid-template-columns: 1fr; } +} diff --git a/languages/_english.json b/languages/_english.json index f531fb390..44d7b5d29 100644 --- a/languages/_english.json +++ b/languages/_english.json @@ -11,6 +11,7 @@ "lang_12_hours": "12 Hours", "lang_12px": "12px", "lang_14_digits": "14 digits", + "lang_15_character_gstin": "15-character GSTIN", "lang_15_days": "15 days", "lang_16px": "16px", "lang_1_day": "1 Day", @@ -108,6 +109,7 @@ "lang_add_kot_2": "+ Add KOT", "lang_add_many_at_once": "Add many at once", "lang_add_new_branch": "Add New Branch", + "lang_add_new_customer_details": "Add new customer details", "lang_add_new_supplier_easily": "Add new supplier easily", "lang_add_new_template": "Add New Template", "lang_add_note": "Add note", @@ -150,6 +152,7 @@ "lang_address": "Address:", "lang_address_2": "Address :", "lang_address_and_phone_print_on_receipts": "Address and phone print on receipts", + "lang_address_delivery_optional": "Address & delivery (optional)", "lang_address_title": "Address", "lang_adds_stock": "Adds stock", "lang_addvariant_title": "New", @@ -260,6 +263,7 @@ "lang_allow_pop_ups_to_print_labels": "Allow pop-ups to print labels", "lang_allow_pop_ups_to_print_the_coupon": "Allow pop-ups to print the coupon.", "lang_allow_sale_hard_delete": "Allow hard delete of sales", + "lang_allow_shared_phone_number": "Allow shared phone number", "lang_allowed": "Allowed", "lang_allowed_confirmed_actions": "Allowed confirmed actions", "lang_already_returned": "Already Returned", @@ -283,11 +287,11 @@ "lang_any": "Any", "lang_any_staff_member_can_swipe_to_clock_in_out": "Any staff member can swipe to clock in / out.", "lang_api": "Api", + "lang_api_integration": "API integration", "lang_api_key": "Api key", "lang_api_key_must_be_at_least_30_characters": "Api key must be at least 30 Characters", "lang_api_version": "API version", "lang_apikey": "Api Key", - "lang_apiuser_title": "Api User", "lang_app": "App", "lang_app_branch": "App / branch", "lang_app_name_must_be_at_least_3_characters": "App Name must be at least 3 Characters", @@ -444,7 +448,6 @@ "lang_bill_2": "Bill", "lang_bill_a_customer_now_get_paid_later_and_see": "Bill a customer now, get paid later - and see who still owes you.", "lang_bill_copies_from_the_floor": "Copies of a bill asked for from the floor", - "lang_bill_customer_details_help": "Optional details for this bill. Use Save new customer to save these details as a customer record.", "lang_bill_date": "Bill date", "lang_bill_date_oldest": "Bill date: oldest", "lang_bill_no": "Bill no", @@ -1039,6 +1042,7 @@ "lang_customer_gstin_hint": "Printed on their bill so they can claim the tax back. Ask for it once and every future bill carries it.", "lang_customer_marketing": "Customer Marketing", "lang_customer_name": "Customer name", + "lang_customer_name_phone": "Customer name / phone", "lang_customer_note": "Note", "lang_customer_options": "Customer options", "lang_customer_phone": "Customer Phone", @@ -1046,7 +1050,6 @@ "lang_customer_records_changed": "Customer records changed", "lang_customer_reference_their_po_job_number": "Customer reference (their PO / job number)", "lang_customer_report": "Customer Report", - "lang_customer_saved": "Customer saved.", "lang_customer_title": "Customers", "lang_customercategory_detail": "Customer Category Details", "lang_customercategory_title": "Customer Categories", @@ -1101,7 +1104,6 @@ "lang_default_template_notes_optional": "Default template / notes (optional)", "lang_defaultcust_title": "Default Customer", "lang_defaultsupply_title": "Default Supplier", - "lang_defaultuser_title": "Default User", "lang_delete": "Delete", "lang_delete_cashinout_title": "Delete Cash In/Out Entry", "lang_delete_denom": "Delete Denom", @@ -1466,7 +1468,6 @@ "lang_enter_a_customer_phone": "Enter a Customer Phone", "lang_enter_a_description": "Enter a description ...", "lang_enter_a_mobile_number": "Enter a mobile number", - "lang_enter_a_new_customer_name_first_or_select": "Enter a new customer name first, or select an existing customer.", "lang_enter_a_non_negative_base_price_and_an_ite": "Enter a non-negative base price and an item markup from 0 to 100000%.", "lang_enter_a_partial_amount": "Enter a partial amount.", "lang_enter_a_password": "Enter a password", @@ -3055,6 +3056,7 @@ "lang_pick_your_provider_and_enter_your_credenti": ". Pick your provider and enter your credentials. Your keys are stored securely on the server and are never shown again after saving.", "lang_pincode_title": "Pincode", "lang_pl_cashbook": "Profit & Loss includes Cash Book", + "lang_place_of_supply_if_different": "Place of supply (if different)", "lang_place_order": "Place order", "lang_plate_ai": "Estimate for me", "lang_plate_ai_failed": "Could not estimate this dish", @@ -3912,6 +3914,8 @@ "lang_salestaxdiscount_title": "Sales Tax Collected", "lang_salestotal_title": "Total Sales", "lang_salt_key": "Salt Key", + "lang_same_as_branch": "Same as branch", + "lang_same_as_customer_branch": "Same as customer / branch", "lang_same_sku": "Same as SKU", "lang_sample_data_choice": "Sample data choice", "lang_sample_data_selected_it_will_install_in_th": "Sample data selected. It will install in the background after Next.", @@ -3931,7 +3935,6 @@ "lang_save_invoice_settings": "Save invoice settings", "lang_save_item": "Save item", "lang_save_loyalty_settings": "Save loyalty settings", - "lang_save_new_customer": "Save new customer", "lang_save_note": "Save note", "lang_save_outlet": "Save outlet", "lang_save_quotation": "Save quotation", @@ -3995,7 +3998,6 @@ "lang_search_calls_pending_settlement_or_review": "Search calls pending settlement or review", "lang_search_category_name": "Search category name", "lang_search_commands": "Search commands", - "lang_search_customer_or_enter_a_new_name": "Search customer or enter a new name", "lang_search_customer_or_invoice": "Search customer or invoice #", "lang_search_customer_or_quote": "Search customer or quote #", "lang_search_features": "Search features", @@ -4213,6 +4215,7 @@ "lang_square": "Square", "lang_ssl_port_465": "SSL (port 465)", "lang_staff": "Staff", + "lang_staff_account": "Staff account", "lang_staff_clock_activity": "Staff clock activity", "lang_staff_clock_in_out_from_the_header_clock_l": "Staff clock in/out from the header clock; labour report and payroll exports.", "lang_staff_name_or_email": "Staff name or email", @@ -4646,7 +4649,6 @@ "lang_url": "URL", "lang_use_a_price_file": "Use a price file", "lang_use_extension_workflows": "Use extension workflows", - "lang_use_on_this_bill": "Use on this bill", "lang_use_one_normal_billing_screen_or_enable_se": "Use one normal billing screen, or enable separate outlets in Features.", "lang_use_sample_data": "Use sample data", "lang_use_suggested": "Use the suggested one", diff --git a/languages/ar.json b/languages/ar.json index 4bde7c9b5..7a759e780 100644 --- a/languages/ar.json +++ b/languages/ar.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF أو JPG أو PNG. ما يصل إلى 5 ملفات، 5 ميغابايت لكل منها، إجمالي 10 ميغابايت. يتم حفظ الملفات عندما تقوم بحفظ الإدخال.", "lang_expense_file_retry": "تم حفظ الإدخال، ولكن تعذر حفظ بعض المرفقات. أبقِ هذا النموذج مفتوحًا واحفظه مرة أخرى لإعادة المحاولة.", "lang_not_saved": "لم يتم حفظها", - "lang_download": "تنزيل" + "lang_download": "تنزيل", + "lang_staff_account": "حساب موظف", + "lang_api_integration": "تكامل API", + "lang_allow_shared_phone_number": "السماح بمشاركة رقم الهاتف", + "lang_same_as_branch": "مثل الفرع", + "lang_15_character_gstin": "رقم GSTIN من 15 حرفًا", + "lang_customer_name_phone": "اسم العميل / الهاتف" } diff --git a/languages/bg.json b/languages/bg.json index 840dfbed2..dd7e44cdb 100644 --- a/languages/bg.json +++ b/languages/bg.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG или PNG. До 5 файла, по 5 MB всеки, общо 10 MB. Файловете се запазват, когато запазите записа.", "lang_expense_file_retry": "Записът е запазен, но някои прикачени файлове не могат да бъдат запазени. Дръжте този формуляр отворен и запазете отново, за да опитате отново.", "lang_not_saved": "Не е запазено", - "lang_download": "Изтегляне" + "lang_download": "Изтегляне", + "lang_staff_account": "Служебен акаунт", + "lang_api_integration": "API интеграция", + "lang_allow_shared_phone_number": "Разрешаване на общ телефонен номер", + "lang_same_as_branch": "Същото като клона", + "lang_15_character_gstin": "GSTIN от 15 знака", + "lang_customer_name_phone": "Име / телефон на клиента" } diff --git a/languages/bn.json b/languages/bn.json index a5b4ba50f..94987f6af 100644 --- a/languages/bn.json +++ b/languages/bn.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "পিডিএফ, জেপিজি বা পিএনজি। 5 টি ফাইল পর্যন্ত, প্রতিটি 5 এমবি, মোট 10 এমবি। আপনি এন্ট্রি সংরক্ষণ করার সময় ফাইলগুলি সংরক্ষণ করা হয়।", "lang_expense_file_retry": "এন্ট্রি সংরক্ষিত, কিন্তু কিছু সংযুক্তি সংরক্ষণ করা যায়নি। এই ফর্মটি খোলা রাখুন এবং আবার চেষ্টা করতে আবার সংরক্ষণ করুন।", "lang_not_saved": "সংরক্ষণ করা হয়নি", - "lang_download": "ডাউনলোড" + "lang_download": "ডাউনলোড", + "lang_staff_account": "কর্মীর অ্যাকাউন্ট", + "lang_api_integration": "API সংযোগ", + "lang_allow_shared_phone_number": "একই ফোন নম্বর ব্যবহারের অনুমতি দিন", + "lang_same_as_branch": "শাখার মতোই", + "lang_15_character_gstin": "১৫ অক্ষরের GSTIN", + "lang_customer_name_phone": "গ্রাহকের নাম / ফোন" } diff --git a/languages/bs.json b/languages/bs.json index 815770f87..08a60273b 100644 --- a/languages/bs.json +++ b/languages/bs.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG ili PNG. Do 5 datoteka, po 5 MB, ukupno 10 MB. Datoteke se čuvaju kada spremite unos.", "lang_expense_file_retry": "Unos je sačuvan, ali neki prilozi nisu mogli biti sačuvani. Držite ovaj obrazac otvoren i ponovno spremite da biste ga ponovo pokušali.", "lang_not_saved": "Nije sačuvano", - "lang_download": "Preuzmi" + "lang_download": "Preuzmi", + "lang_staff_account": "Račun zaposlenika", + "lang_api_integration": "API integracija", + "lang_allow_shared_phone_number": "Dozvoli zajednički broj telefona", + "lang_same_as_branch": "Isto kao poslovnica", + "lang_15_character_gstin": "GSTIN od 15 znakova", + "lang_customer_name_phone": "Ime kupca / telefon" } diff --git a/languages/cs.json b/languages/cs.json index 8fb89d46e..f2e51cb37 100644 --- a/languages/cs.json +++ b/languages/cs.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG nebo PNG. Až 5 souborů, každý po 5 MB, celkem 10 MB. Soubory se uloží při uložení položky.", "lang_expense_file_retry": "Položka byla uložena, ale některé přílohy nemohly být uloženy. Ponechte tento formulář otevřený a znovu jej uložte a zkuste to znovu.", "lang_not_saved": "Neuloženo", - "lang_download": "Stáhnout" + "lang_download": "Stáhnout", + "lang_staff_account": "Účet zaměstnance", + "lang_api_integration": "Integrace API", + "lang_allow_shared_phone_number": "Povolit sdílené telefonní číslo", + "lang_same_as_branch": "Stejné jako pobočka", + "lang_15_character_gstin": "GSTIN s 15 znaky", + "lang_customer_name_phone": "Jméno zákazníka / telefon" } diff --git a/languages/da.json b/languages/da.json index eeab2de8a..18a45832c 100644 --- a/languages/da.json +++ b/languages/da.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG eller PNG. Op til 5 filer, 5 MB hver, 10 MB i alt. Filer gemmes, når du gemmer posten.", "lang_expense_file_retry": "Indtastning gemt, men nogle vedhæftede filer kunne ikke gemmes. Hold denne formular åben, og gem igen for at prøve igen.", "lang_not_saved": "Ikke gemt", - "lang_download": "Hent" + "lang_download": "Hent", + "lang_staff_account": "Medarbejderkonto", + "lang_api_integration": "API-integration", + "lang_allow_shared_phone_number": "Tillad delt telefonnummer", + "lang_same_as_branch": "Samme som filialen", + "lang_15_character_gstin": "GSTIN med 15 tegn", + "lang_customer_name_phone": "Kundenavn / telefon" } diff --git a/languages/de.json b/languages/de.json index 84d21bb33..a7b41aec0 100644 --- a/languages/de.json +++ b/languages/de.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG oder PNG. Bis zu 5 Dateien, jeweils 5 MB, insgesamt 10 MB. Dateien werden gespeichert, wenn Sie den Eintrag speichern.", "lang_expense_file_retry": "Eintrag gespeichert, aber einige Anlagen konnten nicht gespeichert werden. Lassen Sie dieses Formular geöffnet und speichern Sie es erneut, um es erneut zu versuchen.", "lang_not_saved": "Nicht gespeichert", - "lang_download": "Herunterladen" + "lang_download": "Herunterladen", + "lang_staff_account": "Mitarbeiterkonto", + "lang_api_integration": "API-Integration", + "lang_allow_shared_phone_number": "Gemeinsame Telefonnummer erlauben", + "lang_same_as_branch": "Wie die Filiale", + "lang_15_character_gstin": "15-stellige GSTIN", + "lang_customer_name_phone": "Kundenname / Telefon" } diff --git a/languages/el.json b/languages/el.json index 02f060e81..32669b531 100644 --- a/languages/el.json +++ b/languages/el.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG ή PNG. Έως 5 αρχεία, 5 MB το καθένα, 10 MB συνολικά. Τα αρχεία αποθηκεύονται όταν αποθηκεύετε την καταχώρηση.", "lang_expense_file_retry": "Η καταχώρηση αποθηκεύτηκε, αλλά ορισμένα συνημμένα δεν ήταν δυνατή η αποθήκευση. Κρατήστε αυτήν τη φόρμα ανοιχτή και αποθηκεύστε ξανά για να δοκιμάσετε ξανά.", "lang_not_saved": "Δεν αποθηκεύτηκε", - "lang_download": "Κατεβάστε" + "lang_download": "Κατεβάστε", + "lang_staff_account": "Λογαριασμός υπαλλήλου", + "lang_api_integration": "Ενσωμάτωση API", + "lang_allow_shared_phone_number": "Να επιτρέπεται κοινός αριθμός τηλεφώνου", + "lang_same_as_branch": "Ίδιο με το υποκατάστημα", + "lang_15_character_gstin": "GSTIN 15 χαρακτήρων", + "lang_customer_name_phone": "Όνομα πελάτη / τηλέφωνο" } diff --git a/languages/es.json b/languages/es.json index 7ad436baa..aef52f0d7 100644 --- a/languages/es.json +++ b/languages/es.json @@ -4906,5 +4906,11 @@ "lang_expense_files_help": "PDF, JPG o PNG. Hasta 5 archivos de 5 MB cada uno, 10 MB en total. Los archivos se guardan al guardar la entrada.", "lang_expense_file_retry": "Entrada guardada, pero algunos archivos adjuntos no se pudieron guardar. Mantenga este formulario abierto y vuelva a guardarlo para volver a intentarlo.", "lang_not_saved": "No se ha guardado", - "lang_download": "Descargar" + "lang_download": "Descargar", + "lang_staff_account": "Cuenta de personal", + "lang_api_integration": "Integración API", + "lang_allow_shared_phone_number": "Permitir número de teléfono compartido", + "lang_same_as_branch": "Igual que la sucursal", + "lang_15_character_gstin": "GSTIN de 15 caracteres", + "lang_customer_name_phone": "Nombre del cliente / teléfono" } diff --git a/languages/et.json b/languages/et.json index 1bc4fc02f..ad70cfdee 100644 --- a/languages/et.json +++ b/languages/et.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG või PNG. Kuni 5 faili, igaüks 5 MB, kokku 10 MB. Failid salvestatakse kirje salvestamisel.", "lang_expense_file_retry": "Kirje salvestatud, kuid mõnda manust ei õnnestunud salvestada. Hoidke see vorm avatud ja salvestage uuesti proovimiseks.", "lang_not_saved": "Ei salvestata", - "lang_download": "Allalaadimine" + "lang_download": "Allalaadimine", + "lang_staff_account": "Töötaja konto", + "lang_api_integration": "API-integratsioon", + "lang_allow_shared_phone_number": "Luba jagatud telefoninumber", + "lang_same_as_branch": "Sama mis filiaalil", + "lang_15_character_gstin": "15-märgiline GSTIN", + "lang_customer_name_phone": "Kliendi nimi / telefon" } diff --git a/languages/fa.json b/languages/fa.json index 69879e429..ae3667d49 100644 --- a/languages/fa.json +++ b/languages/fa.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "پی دی اف، JPG یا PNG. حداکثر 5 فایل، هر کدام 5 مگابایت، در مجموع 10 مگابایت. هنگامی که ورودی را ذخیره می کنید، فایل ها ذخیره می شوند.", "lang_expense_file_retry": "ورودی ذخیره شد، اما برخی از پیوست ها نمی توانستند ذخیره شوند. این فرم را باز نگه دارید و دوباره ذخیره کنید تا دوباره امتحان کنید.", "lang_not_saved": "ذخیره نشده است", - "lang_download": "دانلود" + "lang_download": "دانلود", + "lang_staff_account": "حساب کارمند", + "lang_api_integration": "یکپارچه‌سازی API", + "lang_allow_shared_phone_number": "اجازه شماره تلفن مشترک", + "lang_same_as_branch": "مانند شعبه", + "lang_15_character_gstin": "GSTIN با ۱۵ نویسه", + "lang_customer_name_phone": "نام مشتری / تلفن" } diff --git a/languages/fi.json b/languages/fi.json index cd2283ce9..ce7b98404 100644 --- a/languages/fi.json +++ b/languages/fi.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG tai PNG. Enintään 5 tiedostoa, 5 Mt kukin, yhteensä 10 Mt. Tiedostot tallennetaan, kun tallennat merkinnän.", "lang_expense_file_retry": "Merkintä tallennettu, mutta joitain liitteitä ei voitu tallentaa. Pidä tämä lomake auki ja tallenna uudelleen yrittääksesi uudelleen.", "lang_not_saved": "Ei tallennettu", - "lang_download": "Lataa" + "lang_download": "Lataa", + "lang_staff_account": "Työntekijätili", + "lang_api_integration": "API-integraatio", + "lang_allow_shared_phone_number": "Salli yhteinen puhelinnumero", + "lang_same_as_branch": "Sama kuin toimipisteellä", + "lang_15_character_gstin": "15 merkin GSTIN", + "lang_customer_name_phone": "Asiakkaan nimi / puhelin" } diff --git a/languages/fr.json b/languages/fr.json index eef5951e0..6c6686b5b 100644 --- a/languages/fr.json +++ b/languages/fr.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG ou PNG. Jusqu'à 5 fichiers de 5 Mo chacun, soit 10 Mo au total. Les fichiers sont enregistrés lorsque vous enregistrez l'entrée.", "lang_expense_file_retry": "Entrée enregistrée, mais certaines pièces jointes n'ont pas pu être enregistrées. Gardez ce formulaire ouvert et enregistrez-le à nouveau pour réessayer.", "lang_not_saved": "Non enregistré", - "lang_download": "Télécharger" + "lang_download": "Télécharger", + "lang_staff_account": "Compte du personnel", + "lang_api_integration": "Intégration API", + "lang_allow_shared_phone_number": "Autoriser un numéro de téléphone partagé", + "lang_same_as_branch": "Identique à la succursale", + "lang_15_character_gstin": "GSTIN de 15 caractères", + "lang_customer_name_phone": "Nom du client / téléphone" } diff --git a/languages/ga.json b/languages/ga.json index 96217a61f..2981c30bf 100644 --- a/languages/ga.json +++ b/languages/ga.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG nó PNG. Suas le 5 chomhad, 5 MB an ceann, 10 MB san iomlán. Sábháiltear comhaid nuair a shábhálann tú an iontráil.", "lang_expense_file_retry": "Sábháil iontráil, ach ní fhéadfaí roinnt ceangaltáin a shábháil. Coinnigh an fhoirm seo oscailte agus sábháil arís chun triail a dhéanamh arís", "lang_not_saved": "Níor sábháilte", - "lang_download": "Íoslódáil" + "lang_download": "Íoslódáil", + "lang_staff_account": "Cuntas foirne", + "lang_api_integration": "Comhtháthú API", + "lang_allow_shared_phone_number": "Ceadaigh uimhir theileafóin chomhroinnte", + "lang_same_as_branch": "Mar an gcéanna leis an mbrainse", + "lang_15_character_gstin": "GSTIN 15 charachtar", + "lang_customer_name_phone": "Ainm an chustaiméara / fón" } diff --git a/languages/gu.json b/languages/gu.json index 53249d9d4..995a0ec79 100644 --- a/languages/gu.json +++ b/languages/gu.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "પીડીએફ, જેપીજી અથવા પીએનજી. 5 ફાઇલો સુધી, 5 એમબી દરેક, કુલ 10 એમબી. જ્યારે તમે એન્ટ્રી સાચવો છો ત્યારે ફાઇલો સાચવવામાં આવે છે.", "lang_expense_file_retry": "એન્ટ્રી સાચવી છે, પરંતુ કેટલાક જોડાણો સાચવી શકાયા નથી. આ ફોર્મ ખુલ્લું રાખો અને ફરીથી પ્રયાસ કરવા માટે ફરીથી સાચવો.", "lang_not_saved": "સાચવવામાં આવ્યું નથી", - "lang_download": "ડાઉનલોડ કરો" + "lang_download": "ડાઉનલોડ કરો", + "lang_staff_account": "કર્મચારી ખાતું", + "lang_api_integration": "API એકીકરણ", + "lang_allow_shared_phone_number": "સમાન ફોન નંબરની મંજૂરી આપો", + "lang_same_as_branch": "શાખા મુજબ", + "lang_15_character_gstin": "15 અક્ષરનો GSTIN", + "lang_customer_name_phone": "ગ્રાહકનું નામ / ફોન" } diff --git a/languages/he.json b/languages/he.json index 5313ef102..9c7629ef7 100644 --- a/languages/he.json +++ b/languages/he.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG או PNG. עד 5 קבצים, 5 מגה בייט כל אחד, 10 מגהבייט בסך הכל. קבצים נשמרים בעת שמירת הערך.", "lang_expense_file_retry": "הערך נשמר, אך לא ניתן לשמור קבצים מצורפים מסוימים. שמור טופס זה פתוח ושמור שוב כדי לנסות שוב.", "lang_not_saved": "לא נשמר", - "lang_download": "הורד" + "lang_download": "הורד", + "lang_staff_account": "חשבון עובד", + "lang_api_integration": "שילוב API", + "lang_allow_shared_phone_number": "אפשר מספר טלפון משותף", + "lang_same_as_branch": "זהה לסניף", + "lang_15_character_gstin": "GSTIN בן 15 תווים", + "lang_customer_name_phone": "שם לקוח / טלפון" } diff --git a/languages/hi.json b/languages/hi.json index 2d9632575..967989b16 100644 --- a/languages/hi.json +++ b/languages/hi.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG या PNG। अधिकतम 5 फ़ाइलें, प्रत्येक में 5 MB, कुल 10 MB। जब आप प्रविष्टि सहेजते हैं तो फ़ाइलें सहेजी जाती हैं।", "lang_expense_file_retry": "प्रविष्टि सहेजी गई, लेकिन कुछ अटैचमेंट सहेजे नहीं जा सके। इस फ़ॉर्म को खुला रखें और फिर से कोशिश करने के लिए फिर से सेव करें।", "lang_not_saved": "सहेजा नहीं गया", - "lang_download": "डाउनलोड करें" + "lang_download": "डाउनलोड करें", + "lang_staff_account": "कर्मचारी खाता", + "lang_api_integration": "API एकीकरण", + "lang_allow_shared_phone_number": "साझा फ़ोन नंबर की अनुमति दें", + "lang_same_as_branch": "शाखा के समान", + "lang_15_character_gstin": "15 अक्षरों का GSTIN", + "lang_customer_name_phone": "ग्राहक का नाम / फ़ोन" } diff --git a/languages/hr.json b/languages/hr.json index 4e9f0ea37..40cb418e1 100644 --- a/languages/hr.json +++ b/languages/hr.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG ili PNG. Do 5 datoteka, po 5 MB, ukupno 10 MB. Datoteke se spremaju kada spremite unos.", "lang_expense_file_retry": "Unos je spreman, ali neki se privitci nisu mogli spremiti. Držite ovaj obrazac otvoren i ponovno spremite da biste ga ponovo pokušali.", "lang_not_saved": "Nije sačuvano", - "lang_download": "Preuzmi" + "lang_download": "Preuzmi", + "lang_staff_account": "Račun zaposlenika", + "lang_api_integration": "API integracija", + "lang_allow_shared_phone_number": "Dopusti zajednički broj telefona", + "lang_same_as_branch": "Isto kao poslovnica", + "lang_15_character_gstin": "GSTIN od 15 znakova", + "lang_customer_name_phone": "Ime kupca / telefon" } diff --git a/languages/hu.json b/languages/hu.json index 655bd5bb3..2f91646b7 100644 --- a/languages/hu.json +++ b/languages/hu.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG vagy PNG. Legfeljebb 5 fájl, egyenként 5 MB, összesen 10 MB. A fájlok mentésekor kerülnek mentésre a bejegyzés.", "lang_expense_file_retry": "A bejegyzés mentett, de néhány mellékletet nem sikerült menteni. Tartsa nyitva ezt az űrlapot, és mentse újra, hogy újra megpróbálkozzon.", "lang_not_saved": "Nem mentett", - "lang_download": "Letöltés" + "lang_download": "Letöltés", + "lang_staff_account": "Munkatársi fiók", + "lang_api_integration": "API-integráció", + "lang_allow_shared_phone_number": "Közös telefonszám engedélyezése", + "lang_same_as_branch": "Az üzlettel megegyező", + "lang_15_character_gstin": "15 karakteres GSTIN", + "lang_customer_name_phone": "Ügyfél neve / telefon" } diff --git a/languages/id.json b/languages/id.json index 63e46c82b..e99205e4c 100644 --- a/languages/id.json +++ b/languages/id.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG atau PNG. Hingga 5 file, masing-masing 5 MB, total 10 MB. File disimpan saat Anda menyimpan entri.", "lang_expense_file_retry": "Entri disimpan, tetapi beberapa lampiran tidak dapat disimpan. Buka formulir ini dan simpan lagi untuk mencoba lagi.", "lang_not_saved": "Tidak disimpan", - "lang_download": "Unduh" + "lang_download": "Unduh", + "lang_staff_account": "Akun staf", + "lang_api_integration": "Integrasi API", + "lang_allow_shared_phone_number": "Izinkan nomor telepon bersama", + "lang_same_as_branch": "Sama dengan cabang", + "lang_15_character_gstin": "GSTIN 15 karakter", + "lang_customer_name_phone": "Nama pelanggan / telepon" } diff --git a/languages/is.json b/languages/is.json index a7d74ff8a..233894e78 100644 --- a/languages/is.json +++ b/languages/is.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG eða PNG. Allt að 5 skrár, 5 MB hver, alls 10 MB. Skrár eru vistaðar þegar þú vistar færsluna.", "lang_expense_file_retry": "Færsla vistuð en ekki var hægt að vista sum viðhengi. Haltu þessu eyðublaði opnu og vistaðu aftur til að reyna aftur.", "lang_not_saved": "Ekki vistað", - "lang_download": "Sækja" + "lang_download": "Sækja", + "lang_staff_account": "Starfsmannaaðgangur", + "lang_api_integration": "API-samþætting", + "lang_allow_shared_phone_number": "Leyfa sameiginlegt símanúmer", + "lang_same_as_branch": "Sama og útibúið", + "lang_15_character_gstin": "15 stafa GSTIN", + "lang_customer_name_phone": "Nafn viðskiptavinar / sími" } diff --git a/languages/it.json b/languages/it.json index 790ffd36b..6ac342fe6 100644 --- a/languages/it.json +++ b/languages/it.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG o PNG. Fino a 5 file, 5 MB ciascuno, 10 MB in totale. I file vengono salvati quando si salva la voce.", "lang_expense_file_retry": "Voce salvata, ma non è stato possibile salvare alcuni allegati. Tieni aperto questo modulo e salva di nuovo per riprovare.", "lang_not_saved": "Non salvato", - "lang_download": "Scarica" + "lang_download": "Scarica", + "lang_staff_account": "Account del personale", + "lang_api_integration": "Integrazione API", + "lang_allow_shared_phone_number": "Consenti numero di telefono condiviso", + "lang_same_as_branch": "Come la filiale", + "lang_15_character_gstin": "GSTIN di 15 caratteri", + "lang_customer_name_phone": "Nome cliente / telefono" } diff --git a/languages/ja.json b/languages/ja.json index ca1f9d804..f8f7d4cf4 100644 --- a/languages/ja.json +++ b/languages/ja.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "PDF、JPG、または PNG。最大 5 つのファイル、それぞれ 5 MB、合計で 10 MB です。エントリを保存すると、ファイルが保存されます。", "lang_expense_file_retry": "エントリは保存されましたが、一部の添付ファイルを保存できませんでした。このフォームを開いたまま保存し直して、再試行してください。", "lang_not_saved": "保存されていません", - "lang_download": "ダウンロード" + "lang_download": "ダウンロード", + "lang_staff_account": "スタッフアカウント", + "lang_api_integration": "API連携", + "lang_allow_shared_phone_number": "電話番号の共有を許可", + "lang_same_as_branch": "店舗と同じ", + "lang_15_character_gstin": "15文字のGSTIN", + "lang_customer_name_phone": "顧客名 / 電話番号" } diff --git a/languages/kn.json b/languages/kn.json index 487b933d4..ad8ae8851 100644 --- a/languages/kn.json +++ b/languages/kn.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "ಪಿಡಿಎಫ್, ಜೆಪಿಜಿ ಅಥವಾ ಪಿಎನ್ಜಿ. 5 ಫೈಲ್ಗಳವರೆಗೆ, ತಲಾ 5 ಎಂಬಿ, ಒಟ್ಟು 10 ಎಂಬಿ. ನೀವು ನಮೂದನ್ನು ಉಳಿಸಿದಾಗ ಫೈಲ್ಗಳನ್ನು ಉಳಿಸಲಾಗುತ್ತದೆ.", "lang_expense_file_retry": "ಎಂಟ್ರಿ ಉಳಿಸಲಾಗಿದೆ, ಆದರೆ ಕೆಲವು ಲಗತ್ತುಗಳನ್ನು ಉಳಿಸಲಾಗಲಿಲ್ಲ. ಈ ಫಾರ್ಮ್ ಅನ್ನು ತೆರೆದಿರಿಸಿ ಮತ್ತು ಮರುಪ್ರಯತ್ನಿಸಲು ಮತ್ತೆ ಉಳಿಸಿ.", "lang_not_saved": "ಉಳಿಸಲಾಗಿಲ್ಲ", - "lang_download": "ಡೌನ್ಲೋಡ್ ಮಾಡಿ" + "lang_download": "ಡೌನ್ಲೋಡ್ ಮಾಡಿ", + "lang_staff_account": "ಸಿಬ್ಬಂದಿ ಖಾತೆ", + "lang_api_integration": "API ಏಕೀಕರಣ", + "lang_allow_shared_phone_number": "ಒಂದೇ ಫೋನ್ ಸಂಖ್ಯೆಯನ್ನು ಬಳಸಲು ಅನುಮತಿಸಿ", + "lang_same_as_branch": "ಶಾಖೆಯಂತೆಯೇ", + "lang_15_character_gstin": "15 ಅಕ್ಷರಗಳ GSTIN", + "lang_customer_name_phone": "ಗ್ರಾಹಕರ ಹೆಸರು / ಫೋನ್" } diff --git a/languages/ko.json b/languages/ko.json index f48998667..cd13a5093 100644 --- a/languages/ko.json +++ b/languages/ko.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "PDF, JPG 또는 PNG최대 5개 파일, 각 5MB, 총 10MB항목을 저장하면 파일이 저장됩니다.", "lang_expense_file_retry": "항목이 저장되었지만 일부 첨부 파일을 저장할 수 없습니다.다시 시도하려면 이 양식을 열어 두었다가 다시 저장하십시오.", "lang_not_saved": "저장되지 않음", - "lang_download": "다운로드" + "lang_download": "다운로드", + "lang_staff_account": "직원 계정", + "lang_api_integration": "API 연동", + "lang_allow_shared_phone_number": "전화번호 공유 허용", + "lang_same_as_branch": "지점과 동일", + "lang_15_character_gstin": "15자 GSTIN", + "lang_customer_name_phone": "고객 이름 / 전화번호" } diff --git a/languages/lt.json b/languages/lt.json index 2390e4009..584141119 100644 --- a/languages/lt.json +++ b/languages/lt.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG arba PNG. Iki 5 failų, po 5 MB, iš viso 10 MB. Failai išsaugomi, kai įrašote įrašą.", "lang_expense_file_retry": "Įrašas išsaugotas, tačiau kai kurių priedų nepavyko išsaugoti. Laikykite šią formą atidarytą ir išsaugokite dar kartą, kad bandytumėte iš naujo.", "lang_not_saved": "Neišsaugota", - "lang_download": "Parsisiųsti" + "lang_download": "Parsisiųsti", + "lang_staff_account": "Darbuotojo paskyra", + "lang_api_integration": "API integracija", + "lang_allow_shared_phone_number": "Leisti bendrą telefono numerį", + "lang_same_as_branch": "Kaip filialo", + "lang_15_character_gstin": "15 simbolių GSTIN", + "lang_customer_name_phone": "Kliento vardas / telefonas" } diff --git a/languages/lv.json b/languages/lv.json index d2d562083..114d20e71 100644 --- a/languages/lv.json +++ b/languages/lv.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG vai PNG. Līdz 5 failiem, katrs 5 MB, kopā 10 MB. Faili tiek saglabāti, saglabājot ierakstu.", "lang_expense_file_retry": "Ieraksts saglabāts, bet dažus pielikumus nevarēja saglabāt. Saglabājiet šo veidlapu atvērtu un saglabājiet vēlreiz, lai mēģinātu atkārtoti.", "lang_not_saved": "Nav saglabāts", - "lang_download": "Lejupielādēt" + "lang_download": "Lejupielādēt", + "lang_staff_account": "Darbinieka konts", + "lang_api_integration": "API integrācija", + "lang_allow_shared_phone_number": "Atļaut kopīgu tālruņa numuru", + "lang_same_as_branch": "Tāpat kā filiālei", + "lang_15_character_gstin": "15 rakstzīmju GSTIN", + "lang_customer_name_phone": "Klienta vārds / tālrunis" } diff --git a/languages/mk.json b/languages/mk.json index 7bf3adb58..172e18001 100644 --- a/languages/mk.json +++ b/languages/mk.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG или PNG. До 5 датотеки, по 5 MB, вкупно 10 MB. Датотеките се зачувуваат кога ќе го зачувате записот.", "lang_expense_file_retry": "Влезот е зачуван, но некои прилози не можеа да се зачуваат. Чувајте го овој формулар отворен и зачувајте повторно за да се обидете повторно.", "lang_not_saved": "Не е зачуван", - "lang_download": "Преземи" + "lang_download": "Преземи", + "lang_staff_account": "Сметка за вработен", + "lang_api_integration": "API интеграција", + "lang_allow_shared_phone_number": "Дозволи заеднички телефонски број", + "lang_same_as_branch": "Исто како филијалата", + "lang_15_character_gstin": "GSTIN од 15 знаци", + "lang_customer_name_phone": "Име на клиент / телефон" } diff --git a/languages/ml.json b/languages/ml.json index 390be2e00..074cdf3fb 100644 --- a/languages/ml.json +++ b/languages/ml.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "പിഡിഎഫ്, ജെപിജി അല്ലെങ്കിൽ പിഎൻജി. 5 ഫയലുകൾ വരെ, 5 എംബി വീതം, മൊത്തം 10 എംബി. നിങ്ങൾ എൻട്രി സേവ് ചെയ്യുമ്പോൾ ഫയലുകൾ സംരക്ഷിക്കപ്പെടും.", "lang_expense_file_retry": "എൻട്രി സംരക്ഷിച്ചു, എന്നാൽ ചില അറ്റാച്ചുമെന്റുകൾ സംരക്ഷിക്കാൻ കഴിഞ്ഞില്ല. വീണ്ടും ശ്രമിക്കുന്നതിന് ഈ ഫോം തുറന്ന് വീണ്ടും സംരക്ഷിക്കുക.", "lang_not_saved": "സംരക്ഷിച്ചില്ല", - "lang_download": "ഡൗൺലോഡ്" + "lang_download": "ഡൗൺലോഡ്", + "lang_staff_account": "ജീവനക്കാരുടെ അക്കൗണ്ട്", + "lang_api_integration": "API സംയോജനം", + "lang_allow_shared_phone_number": "ഒരേ ഫോൺ നമ്പർ ഉപയോഗിക്കാൻ അനുവദിക്കുക", + "lang_same_as_branch": "ശാഖയുടേതിന് സമാനം", + "lang_15_character_gstin": "15 അക്ഷരങ്ങളുള്ള GSTIN", + "lang_customer_name_phone": "ഉപഭോക്താവിന്റെ പേര് / ഫോൺ" } diff --git a/languages/mr.json b/languages/mr.json index b3b9e27cc..d51d33df4 100644 --- a/languages/mr.json +++ b/languages/mr.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "पीडीएफ, जेपीजी किंवा PNG. 5 फायली पर्यंत, प्रत्येक 5 एमबी, एकूण 10 एमबी जेव्हा आपण एंट्री जतन करता तेव्हा फाइल्", "lang_expense_file_retry": "प्रवेश जतन केला, परंतु काही संलग्नक जतन केले हा फॉर्म खुला ठेवा आणि पुन्हा प्रयत्न करण्यासाठी पुन्ह", "lang_not_saved": "जतन केलेले नाही", - "lang_download": "डानलोड" + "lang_download": "डानलोड", + "lang_staff_account": "कर्मचारी खाते", + "lang_api_integration": "API एकत्रीकरण", + "lang_allow_shared_phone_number": "सामायिक फोन नंबरला परवानगी द्या", + "lang_same_as_branch": "शाखेप्रमाणेच", + "lang_15_character_gstin": "15 अक्षरांचा GSTIN", + "lang_customer_name_phone": "ग्राहकाचे नाव / फोन" } diff --git a/languages/ms.json b/languages/ms.json index 3922cda36..cdcd43433 100644 --- a/languages/ms.json +++ b/languages/ms.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "PDF, JPG atau PNG. Sehingga 5 fail, masing-masing 5 MB, jumlah 10 MB. Fail disimpan apabila anda menyimpan entri.", "lang_expense_file_retry": "Kemasukan disimpan, tetapi beberapa lampiran tidak dapat disimpan. Pastikan borang ini terbuka dan simpan lagi untuk mencuba semula.", "lang_not_saved": "Tidak disimpan", - "lang_download": "Muat turun" + "lang_download": "Muat turun", + "lang_staff_account": "Akaun kakitangan", + "lang_api_integration": "Integrasi API", + "lang_allow_shared_phone_number": "Benarkan nombor telefon dikongsi", + "lang_same_as_branch": "Sama seperti cawangan", + "lang_15_character_gstin": "GSTIN 15 aksara", + "lang_customer_name_phone": "Nama pelanggan / telefon" } diff --git a/languages/mt.json b/languages/mt.json index 80ff3b8be..892c56326 100644 --- a/languages/mt.json +++ b/languages/mt.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG jew PNG. Sa 5 fajls, 5 MB kull wieħed, total ta' 10 MB. Il-fajls jiġu ffrankati meta tiffranka l-entrata.", "lang_expense_file_retry": "Id-dħul salvat, iżda xi annessi ma setgħux jiġu ffrankati. Żomm din il-formola miftuħa u ssalva mill-ġdid biex terġa 'tipprova.", "lang_not_saved": "Mhux salvat", - "lang_download": "Niżżel" + "lang_download": "Niżżel", + "lang_staff_account": "Kont tal-persunal", + "lang_api_integration": "Integrazzjoni API", + "lang_allow_shared_phone_number": "Ħalli numru tat-telefon kondiviż", + "lang_same_as_branch": "L-istess bħall-fergħa", + "lang_15_character_gstin": "GSTIN ta’ 15-il karattru", + "lang_customer_name_phone": "Isem il-klijent / telefon" } diff --git a/languages/nb.json b/languages/nb.json index b118474b8..df7f788b7 100644 --- a/languages/nb.json +++ b/languages/nb.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG eller PNG. Opptil 5 filer, 5 MB hver, 10 MB totalt. Filer lagres når du lagrer oppføringen.", "lang_expense_file_retry": "Oppføring lagret, men noen vedlegg kunne ikke lagres. Hold dette skjemaet åpent og lagre igjen for å prøve på nytt.", "lang_not_saved": "Ikke lagret", - "lang_download": "Last ned" + "lang_download": "Last ned", + "lang_staff_account": "Ansattkonto", + "lang_api_integration": "API-integrasjon", + "lang_allow_shared_phone_number": "Tillat delt telefonnummer", + "lang_same_as_branch": "Samme som avdelingen", + "lang_15_character_gstin": "GSTIN med 15 tegn", + "lang_customer_name_phone": "Kundenavn / telefon" } diff --git a/languages/ne.json b/languages/ne.json index a6f7c75c6..996fbf6cd 100644 --- a/languages/ne.json +++ b/languages/ne.json @@ -4553,5 +4553,15 @@ "lang_name_or_phone": "नाम वा फोन", "lang_save_new_customer": "नयाँ ग्राहक सुरक्षित गर्नुहोस्", "lang_saving_customer": "ग्राहक सुरक्षित गर्दै...", - "lang_search_customer_or_enter_a_new_name": "ग्राहक खोज्नुहोस् वा नयाँ नाम लेख्नुहोस्" + "lang_search_customer_or_enter_a_new_name": "ग्राहक खोज्नुहोस् वा नयाँ नाम लेख्नुहोस्", + "lang_staff_account": "कर्मचारी खाता", + "lang_api_integration": "API एकीकरण", + "lang_allow_shared_phone_number": "साझा फोन नम्बर अनुमति दिनुहोस्", + "lang_same_as_branch": "शाखा जस्तै", + "lang_15_character_gstin": "१५ अक्षरको GSTIN", + "lang_customer_name_phone": "ग्राहकको नाम / फोन", + "lang_add_new_customer_details": "नयाँ ग्राहकको विवरण थप्नुहोस्", + "lang_address_delivery_optional": "ठेगाना र डेलिभरी (वैकल्पिक)", + "lang_place_of_supply_if_different": "आपूर्तिको स्थान (फरक भएमा)", + "lang_same_as_customer_branch": "ग्राहक / शाखा जस्तै" } diff --git a/languages/nl.json b/languages/nl.json index 1d5b994ac..88c849687 100644 --- a/languages/nl.json +++ b/languages/nl.json @@ -4986,5 +4986,15 @@ "lang_name_or_phone": "Naam of telefoonnummer", "lang_save_new_customer": "Nieuwe klant opslaan", "lang_saving_customer": "Klant opslaan...", - "lang_search_customer_or_enter_a_new_name": "Zoek een klant of voer een nieuwe naam in" + "lang_search_customer_or_enter_a_new_name": "Zoek een klant of voer een nieuwe naam in", + "lang_staff_account": "Medewerkersaccount", + "lang_api_integration": "API-integratie", + "lang_allow_shared_phone_number": "Gedeeld telefoonnummer toestaan", + "lang_same_as_branch": "Gelijk aan vestiging", + "lang_15_character_gstin": "GSTIN van 15 tekens", + "lang_customer_name_phone": "Klantnaam / telefoon", + "lang_add_new_customer_details": "Nieuwe klantgegevens toevoegen", + "lang_address_delivery_optional": "Adres en bezorging (optioneel)", + "lang_place_of_supply_if_different": "Plaats van levering (indien afwijkend)", + "lang_same_as_customer_branch": "Gelijk aan klant / vestiging" } diff --git a/languages/pa.json b/languages/pa.json index f87283027..178ef763c 100644 --- a/languages/pa.json +++ b/languages/pa.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "ਪੀਡੀਐਫ, ਜੇਪੀਜੀ ਜਾਂ ਪੀ ਐਨ ਜੀ. 5 ਫਾਈਲਾਂ ਤੱਕ, ਹਰੇਕ 5 MB, ਕੁੱਲ 10 MB। ਜਦੋਂ ਤੁਸੀਂ ਐਂਟਰੀ ਸੁਰੱਖਿਅਤ ਕਰਦੇ ਹੋ ਤਾਂ ਫਾਈਲਾਂ ਸੁਰੱਖਿਅਤ ਕੀਤੀਆਂ ਜਾਂਦੀਆਂ ਹਨ।", "lang_expense_file_retry": "ਐਂਟਰੀ ਸੁਰੱਖਿਅਤ ਕੀਤੀ ਗਈ, ਪਰ ਕੁਝ ਅਟੈਚਮੈਂਟ ਸੁਰੱਖਿਅਤ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ. ਇਸ ਫਾਰਮ ਨੂੰ ਖੁੱਲਾ ਰੱਖੋ ਅਤੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰਨ ਲਈ ਦੁਬਾਰਾ ਸੁਰੱਖਿਅਤ ਕਰੋ.", "lang_not_saved": "ਸੁਰੱਖਿਅਤ ਨਹੀਂ", - "lang_download": "ਡਾਊਨਲੋਡ" + "lang_download": "ਡਾਊਨਲੋਡ", + "lang_staff_account": "ਕਰਮਚਾਰੀ ਖਾਤਾ", + "lang_api_integration": "API ਏਕੀਕਰਨ", + "lang_allow_shared_phone_number": "ਸਾਂਝੇ ਫ਼ੋਨ ਨੰਬਰ ਦੀ ਇਜਾਜ਼ਤ ਦਿਓ", + "lang_same_as_branch": "ਸ਼ਾਖਾ ਵਾਂਗ", + "lang_15_character_gstin": "15 ਅੱਖਰਾਂ ਦਾ GSTIN", + "lang_customer_name_phone": "ਗਾਹਕ ਦਾ ਨਾਮ / ਫ਼ੋਨ" } diff --git a/languages/pl.json b/languages/pl.json index ece33d05f..17c8a8620 100644 --- a/languages/pl.json +++ b/languages/pl.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG lub PNG. Do 5 plików, po 5 MB każdy, łącznie 10 MB. Pliki są zapisywane podczas zapisywania wpisu.", "lang_expense_file_retry": "Wpis został zapisany, ale niektórych załączników nie można zapisać. Pozostaw ten formularz otwarty i zapisz ponownie, aby spróbować ponownie.", "lang_not_saved": "Nie zapisano", - "lang_download": "Ściągnij" + "lang_download": "Ściągnij", + "lang_staff_account": "Konto pracownika", + "lang_api_integration": "Integracja API", + "lang_allow_shared_phone_number": "Zezwalaj na wspólny numer telefonu", + "lang_same_as_branch": "Tak samo jak oddział", + "lang_15_character_gstin": "15-znakowy GSTIN", + "lang_customer_name_phone": "Nazwa klienta / telefon" } diff --git a/languages/pt.json b/languages/pt.json index 909307fdb..5d8433e45 100644 --- a/languages/pt.json +++ b/languages/pt.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG ou PNG. Até 5 arquivos, 5 MB cada, 10 MB no total. Os arquivos são salvos quando você salva a entrada.", "lang_expense_file_retry": "A entrada foi salva, mas alguns anexos não puderam ser salvos. Mantenha esse formulário aberto e salve-o novamente para tentar novamente.", "lang_not_saved": "Não salvo", - "lang_download": "Baixar" + "lang_download": "Baixar", + "lang_staff_account": "Conta de funcionário", + "lang_api_integration": "Integração API", + "lang_allow_shared_phone_number": "Permitir número de telefone partilhado", + "lang_same_as_branch": "Igual à filial", + "lang_15_character_gstin": "GSTIN de 15 caracteres", + "lang_customer_name_phone": "Nome do cliente / telefone" } diff --git a/languages/ro.json b/languages/ro.json index dfabcf471..45da41eec 100644 --- a/languages/ro.json +++ b/languages/ro.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG sau PNG. Până la 5 fișiere, câte 5 MB fiecare, 10 MB total. Fișierele sunt salvate atunci când salvați intrarea.", "lang_expense_file_retry": "Intrarea a fost salvată, dar unele atașamente nu au putut fi salvate. Păstrați acest formular deschis și salvați din nou pentru a încerca din nou.", "lang_not_saved": "Nu a fost salvat", - "lang_download": "Descarca" + "lang_download": "Descarca", + "lang_staff_account": "Cont de angajat", + "lang_api_integration": "Integrare API", + "lang_allow_shared_phone_number": "Permite număr de telefon comun", + "lang_same_as_branch": "La fel ca sucursala", + "lang_15_character_gstin": "GSTIN de 15 caractere", + "lang_customer_name_phone": "Nume client / telefon" } diff --git a/languages/ru.json b/languages/ru.json index db745c6ae..8be812a9c 100644 --- a/languages/ru.json +++ b/languages/ru.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "PDF, JPG или PNG. До 5 файлов по 5 МБ каждый, всего 10 МБ. Файлы сохраняются при сохранении записи.", "lang_expense_file_retry": "Запись сохранена, но некоторые вложения сохранить не удалось. Оставьте эту форму открытой и снова сохраните ее, чтобы повторить попытку.", "lang_not_saved": "Не сохранено", - "lang_download": "Загрузить" + "lang_download": "Загрузить", + "lang_staff_account": "Учётная запись сотрудника", + "lang_api_integration": "Интеграция API", + "lang_allow_shared_phone_number": "Разрешить общий номер телефона", + "lang_same_as_branch": "Как у филиала", + "lang_15_character_gstin": "GSTIN из 15 символов", + "lang_customer_name_phone": "Имя клиента / телефон" } diff --git a/languages/si.json b/languages/si.json index 848e33872..af5baa454 100644 --- a/languages/si.json +++ b/languages/si.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG හෝ PNG. ගොනු 5 ක් දක්වා, 5 MB බැගින්, මුළු 10 MB. ඔබ ඇතුළත් කිරීම සුරකින විට ගොනු සුරකිනු ලැබේ.", "lang_expense_file_retry": "ඇතුළත් කිරීම සුරකින ලද නමුත් සමහර ඇමුණුම් සුරැකීමට නොහැකි විය. මෙම පෝරමය විවෘතව තබා නැවත උත්සාහ කිරීම සඳහා නැවත සුරකින්න.", "lang_not_saved": "සුරකිනු නොමැත", - "lang_download": "බාගත කරන්න" + "lang_download": "බාගත කරන්න", + "lang_staff_account": "සේවක ගිණුම", + "lang_api_integration": "API ඒකාබද්ධ කිරීම", + "lang_allow_shared_phone_number": "හවුල් දුරකථන අංකයට අවසර දෙන්න", + "lang_same_as_branch": "ශාඛාවට සමානයි", + "lang_15_character_gstin": "අක්ෂර 15ක GSTIN", + "lang_customer_name_phone": "පාරිභෝගික නම / දුරකථනය" } diff --git a/languages/sk.json b/languages/sk.json index a2f7aa6d6..74b49a751 100644 --- a/languages/sk.json +++ b/languages/sk.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG alebo PNG. Až 5 súborov, každý 5 MB, celkom 10 MB. Súbory sa ukladajú pri uložení položky.", "lang_expense_file_retry": "Záznam bol uložený, ale niektoré prílohy sa nepodarilo uložiť. Tento formulár nechajte otvorený a znova ho uložte, aby ste to skúsili znova.", "lang_not_saved": "Nie je uložený", - "lang_download": "Stiahnite si" + "lang_download": "Stiahnite si", + "lang_staff_account": "Účet zamestnanca", + "lang_api_integration": "Integrácia API", + "lang_allow_shared_phone_number": "Povoliť zdieľané telefónne číslo", + "lang_same_as_branch": "Rovnaké ako pobočka", + "lang_15_character_gstin": "GSTIN s 15 znakmi", + "lang_customer_name_phone": "Meno zákazníka / telefón" } diff --git a/languages/sl.json b/languages/sl.json index 052bf2ade..3df90d790 100644 --- a/languages/sl.json +++ b/languages/sl.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG ali PNG. Do 5 datotek, po 5 MB, skupaj 10 MB. Datoteke se shranijo, ko shranite vnos.", "lang_expense_file_retry": "Vnos je shranjen, vendar nekaterih prilog ni bilo mogoče shraniti. Ta obrazec naj bo odprt in ponovno shranite, da poskusite znova.", "lang_not_saved": "Ni shranjeno", - "lang_download": "Prenesi" + "lang_download": "Prenesi", + "lang_staff_account": "Račun zaposlenega", + "lang_api_integration": "Integracija API", + "lang_allow_shared_phone_number": "Dovoli skupno telefonsko številko", + "lang_same_as_branch": "Enako kot poslovalnica", + "lang_15_character_gstin": "GSTIN s 15 znaki", + "lang_customer_name_phone": "Ime stranke / telefon" } diff --git a/languages/sq.json b/languages/sq.json index efd1c8454..f42a32bb9 100644 --- a/languages/sq.json +++ b/languages/sq.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG ose PNG. Deri në 5 skedarë, 5 MB secila, 10 MB gjithsej. Skedarët ruhen kur ruani hyrjen.", "lang_expense_file_retry": "Hyrja është ruajtur, por disa bashkëngjitje nuk mund të ruhen. Mbani këtë formular të hapur dhe ruani përsëri për të riprovuar.", "lang_not_saved": "Nuk është ruajtur", - "lang_download": "Shkarko" + "lang_download": "Shkarko", + "lang_staff_account": "Llogari punonjësi", + "lang_api_integration": "Integrim API", + "lang_allow_shared_phone_number": "Lejo numër telefoni të përbashkët", + "lang_same_as_branch": "Njësoj si dega", + "lang_15_character_gstin": "GSTIN me 15 karaktere", + "lang_customer_name_phone": "Emri i klientit / telefoni" } diff --git a/languages/sr.json b/languages/sr.json index 760c42274..8ffc90c55 100644 --- a/languages/sr.json +++ b/languages/sr.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG ili PNG. Do 5 datoteka, po 5 MB, ukupno 10 MB. Datoteke se čuvaju kada sačuvate unos.", "lang_expense_file_retry": "Unos je sačuvan, ali neki prilozi nisu mogli da se sačuvaju. Držite ovaj obrazac otvoren i ponovo sačuvajte da biste ga ponovo pokušali.", "lang_not_saved": "Nije sačuvano", - "lang_download": "Preuzmi" + "lang_download": "Preuzmi", + "lang_staff_account": "Налог запосленог", + "lang_api_integration": "API интеграција", + "lang_allow_shared_phone_number": "Дозволи заједнички број телефона", + "lang_same_as_branch": "Исто као пословница", + "lang_15_character_gstin": "GSTIN од 15 знакова", + "lang_customer_name_phone": "Име купца / телефон" } diff --git a/languages/sv.json b/languages/sv.json index 6e8d85b8d..db8c65c8f 100644 --- a/languages/sv.json +++ b/languages/sv.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG eller PNG. Upp till 5 filer, 5 MB vardera, 10 MB totalt. Filer sparas när du sparar posten.", "lang_expense_file_retry": "Posten sparades, men vissa bilagor kunde inte sparas. Håll det här formuläret öppet och spara igen för att försöka igen.", "lang_not_saved": "Inte sparad", - "lang_download": "Ladda ner" + "lang_download": "Ladda ner", + "lang_staff_account": "Personalkonto", + "lang_api_integration": "API-integration", + "lang_allow_shared_phone_number": "Tillåt delat telefonnummer", + "lang_same_as_branch": "Samma som filialen", + "lang_15_character_gstin": "GSTIN med 15 tecken", + "lang_customer_name_phone": "Kundnamn / telefon" } diff --git a/languages/sw.json b/languages/sw.json index c04eda878..2003e01be 100644 --- a/languages/sw.json +++ b/languages/sw.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG au PNG. Hadi faili 5, 5 MB kila moja, jumla ya 10 MB. Faili zinahifadhiwa wakati unapohifadhi ingilio.", "lang_expense_file_retry": "Kuingia imehifadhiwa, lakini viambatisho vingine havikuweza kuhifadhi Weka fomu hii wazi na uhifadhi tena ili kujaribu tena.", "lang_not_saved": "Haikuokolewa", - "lang_download": "Pakua" + "lang_download": "Pakua", + "lang_staff_account": "Akaunti ya mfanyakazi", + "lang_api_integration": "Muunganisho wa API", + "lang_allow_shared_phone_number": "Ruhusu nambari ya simu ya pamoja", + "lang_same_as_branch": "Sawa na tawi", + "lang_15_character_gstin": "GSTIN yenye herufi 15", + "lang_customer_name_phone": "Jina la mteja / simu" } diff --git a/languages/ta.json b/languages/ta.json index c3c36bb54..cd72727b1 100644 --- a/languages/ta.json +++ b/languages/ta.json @@ -4914,5 +4914,11 @@ "lang_expense_files_help": "PDF, JPG அல்லது PNG. 5 கோப்புகள் வரை, தலா 5 எம்பி, மொத்தம் 10 எம்பி. நீங்கள் உள்ளீட்டைச் சேமிக்கும்போது கோப்புகள் சேமிக்கப்படும்.", "lang_expense_file_retry": "உள்ளீடு சேமிக்கப்பட்டது, ஆனால் சில இணைப்புகளை சேமிக்க முடியவில்லை. இந்த படிவத்தை திறந்து மீண்டும் முயற்சிக்க மீண்டும் சேமிக்கவும்.", "lang_not_saved": "சேமிக்கப்படவில்லை", - "lang_download": "பதிவிறக்க" + "lang_download": "பதிவிறக்க", + "lang_staff_account": "பணியாளர் கணக்கு", + "lang_api_integration": "API ஒருங்கிணைப்பு", + "lang_allow_shared_phone_number": "பகிரப்பட்ட தொலைபேசி எண்ணை அனுமதி", + "lang_same_as_branch": "கிளையைப் போன்றே", + "lang_15_character_gstin": "15 எழுத்துகள் கொண்ட GSTIN", + "lang_customer_name_phone": "வாடிக்கையாளர் பெயர் / தொலைபேசி" } diff --git a/languages/te.json b/languages/te.json index 526e02b14..69576b36a 100644 --- a/languages/te.json +++ b/languages/te.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "పిడిఎఫ్, జెపిజి లేదా పిఎన్జి. 5 ఫైల్స్ వరకు, ఒక్కొక్కటి 5 MB, మొత్తం 10 MB. మీరు ఎంట్రీని సేవ్ చేసినప్పుడు ఫైల్లు సేవ్ చేయబడతాయి.", "lang_expense_file_retry": "ఎంట్రీ సేవ్ చేయబడింది, కానీ కొన్ని జోడింపులను సేవ్ చేయలేకపోయింది. ఈ ఫారమ్ను తెరిచి ఉంచండి మరియు తిరిగి ప్రయత్నించడానికి మళ్ళీ సేవ్ చేయండి.", "lang_not_saved": "సేవ్ చేయబడలేదు", - "lang_download": "డౌన్లోడ్" + "lang_download": "డౌన్లోడ్", + "lang_staff_account": "సిబ్బంది ఖాతా", + "lang_api_integration": "API అనుసంధానం", + "lang_allow_shared_phone_number": "ఒకే ఫోన్ నంబర్ వాడటానికి అనుమతించండి", + "lang_same_as_branch": "శాఖ మాదిరిగానే", + "lang_15_character_gstin": "15 అక్షరాల GSTIN", + "lang_customer_name_phone": "కస్టమర్ పేరు / ఫోన్" } diff --git a/languages/th.json b/languages/th.json index 591ea4726..9b1d6ad89 100644 --- a/languages/th.json +++ b/languages/th.json @@ -4910,5 +4910,11 @@ "lang_expense_files_help": "PDF, JPG หรือ PNGสูงสุด 5 ไฟล์ ละ 5 เมกะไบต์ รวม 10 MBไฟล์จะถูกบันทึกเมื่อคุณบันทึกรายการ", "lang_expense_file_retry": "รายการบันทึกไว้ แต่ไม่สามารถบันทึกไฟล์แนบบางรายการได้เปิดแบบฟอร์มนี้ไว้และบันทึกอีกครั้งเพื่อลองอีกครั้ง", "lang_not_saved": "ไม่ได้บันทึก", - "lang_download": "ดาวน์โหลด" + "lang_download": "ดาวน์โหลด", + "lang_staff_account": "บัญชีพนักงาน", + "lang_api_integration": "การเชื่อมต่อ API", + "lang_allow_shared_phone_number": "อนุญาตให้ใช้หมายเลขโทรศัพท์ร่วมกัน", + "lang_same_as_branch": "เหมือนสาขา", + "lang_15_character_gstin": "GSTIN 15 ตัวอักษร", + "lang_customer_name_phone": "ชื่อลูกค้า / โทรศัพท์" } diff --git a/languages/tl.json b/languages/tl.json index c378ebec2..174a66b77 100644 --- a/languages/tl.json +++ b/languages/tl.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG o PNG. Hanggang sa 5 mga file, 5 MB bawat isa, 10 MB kabuuang. Ang mga file ay nai-save kapag nai-save mo ang entry.", "lang_expense_file_retry": "Naka-save ang entry, ngunit ang ilang mga attachment ay hindi mai-save. Panatilihing bukas ang form na ito at i-save muli upang subukang muli.", "lang_not_saved": "Hindi nai-save", - "lang_download": "I-download" + "lang_download": "I-download", + "lang_staff_account": "Account ng kawani", + "lang_api_integration": "Integrasyon ng API", + "lang_allow_shared_phone_number": "Payagan ang pinagsasaluhang numero ng telepono", + "lang_same_as_branch": "Katulad ng sangay", + "lang_15_character_gstin": "GSTIN na may 15 karakter", + "lang_customer_name_phone": "Pangalan ng customer / telepono" } diff --git a/languages/tr.json b/languages/tr.json index 3eb112e37..0febfcbf0 100644 --- a/languages/tr.json +++ b/languages/tr.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "PDF, JPG veya PNG. 5 adede kadar dosya, her biri 5 MB, toplam 10 MB. Girdi kaydettiğinizde dosyalar kaydedilir.", "lang_expense_file_retry": "Giriş kaydedildi, ancak bazı ekler kaydedilemedi. Bu formu açık tutun ve tekrar denemek için tekrar kaydedin.", "lang_not_saved": "Kaydedilmedi", - "lang_download": "İndir" + "lang_download": "İndir", + "lang_staff_account": "Personel hesabı", + "lang_api_integration": "API entegrasyonu", + "lang_allow_shared_phone_number": "Ortak telefon numarasına izin ver", + "lang_same_as_branch": "Şubeyle aynı", + "lang_15_character_gstin": "15 karakterli GSTIN", + "lang_customer_name_phone": "Müşteri adı / telefon" } diff --git a/languages/uk.json b/languages/uk.json index 1df9d8df4..754fb4796 100644 --- a/languages/uk.json +++ b/languages/uk.json @@ -4828,5 +4828,11 @@ "lang_expense_files_help": "PDF, JPG або PNG. До 5 файлів, по 5 МБ кожен, 10 МБ загалом. Файли зберігаються при збереженні запису.", "lang_expense_file_retry": "Запис збережено, але деякі вкладення не вдалося зберегти. Зберігайте цю форму відкритою та збережіть її знову, щоб повторити спробу.", "lang_not_saved": "Не збережено", - "lang_download": "Завантажити" + "lang_download": "Завантажити", + "lang_staff_account": "Обліковий запис працівника", + "lang_api_integration": "Інтеграція API", + "lang_allow_shared_phone_number": "Дозволити спільний номер телефону", + "lang_same_as_branch": "Як у філії", + "lang_15_character_gstin": "GSTIN із 15 символів", + "lang_customer_name_phone": "Ім’я клієнта / телефон" } diff --git a/languages/ur.json b/languages/ur.json index 295f60fac..351062bf6 100644 --- a/languages/ur.json +++ b/languages/ur.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "پی ڈی ایف، جے پی جی یا پی این جی۔ 5 فائلیں تک، ہر ایک 5 ایم بی، کل 10 ایم بی۔ جب آپ اندراج محفوظ کرتے ہیں تو فائلیں محفوظ ہوجاتی ہیں۔", "lang_expense_file_retry": "انٹری محفوظ ہوئی، لیکن کچھ منسلکات محفوظ نہیں ہوسکے۔ اس فارم کو کھلا رکھیں اور دوبارہ کوشش کرنے کے لیے دوبارہ محفوظ کریں۔", "lang_not_saved": "محفوظ نہیں", - "lang_download": "ڈاؤن لوڈ" + "lang_download": "ڈاؤن لوڈ", + "lang_staff_account": "ملازم کا اکاؤنٹ", + "lang_api_integration": "API انضمام", + "lang_allow_shared_phone_number": "مشترکہ فون نمبر کی اجازت دیں", + "lang_same_as_branch": "برانچ جیسا", + "lang_15_character_gstin": "15 حروف کا GSTIN", + "lang_customer_name_phone": "گاہک کا نام / فون" } diff --git a/languages/vi.json b/languages/vi.json index 1a4b53e47..ef5fae5c0 100644 --- a/languages/vi.json +++ b/languages/vi.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "PDF, JPG hoặc PNG. Tối đa 5 tệp, mỗi tệp 5 MB, tổng cộng 10 MB. Các tệp được lưu khi bạn lưu mục nhập.", "lang_expense_file_retry": "Mục nhập đã lưu, nhưng một số tệp đính kèm không thể được lưu. Giữ biểu mẫu này mở và lưu lại để thử lại.", "lang_not_saved": "Không được lưu", - "lang_download": "Tải về" + "lang_download": "Tải về", + "lang_staff_account": "Tài khoản nhân viên", + "lang_api_integration": "Tích hợp API", + "lang_allow_shared_phone_number": "Cho phép dùng chung số điện thoại", + "lang_same_as_branch": "Giống chi nhánh", + "lang_15_character_gstin": "GSTIN gồm 15 ký tự", + "lang_customer_name_phone": "Tên khách hàng / điện thoại" } diff --git a/languages/zh-CN.json b/languages/zh-CN.json index 81742ba15..9e2bd7b2d 100644 --- a/languages/zh-CN.json +++ b/languages/zh-CN.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "PDF、JPG 或 PNG。最多 5 个文件,每个文件 5 MB,总计 10 MB。保存条目时会保存文件。", "lang_expense_file_retry": "条目已保存,但无法保存某些附件。保持此表单处于打开状态,然后再次保存以重试。", "lang_not_saved": "未保存", - "lang_download": "下載" + "lang_download": "下載", + "lang_staff_account": "员工账户", + "lang_api_integration": "API 集成", + "lang_allow_shared_phone_number": "允许共用电话号码", + "lang_same_as_branch": "与分店相同", + "lang_15_character_gstin": "15位GSTIN", + "lang_customer_name_phone": "客户姓名 / 电话" } diff --git a/languages/zh-TW.json b/languages/zh-TW.json index 86eed3f34..c4451e9be 100644 --- a/languages/zh-TW.json +++ b/languages/zh-TW.json @@ -4831,5 +4831,11 @@ "lang_expense_files_help": "PDF,JPG 或 PNG。最多 5 個文件,每個 5 MB,總共 10 MB。儲存項目時,檔案會儲存。", "lang_expense_file_retry": "項目已儲存,但部分附件無法儲存。保持此表單打開並再次儲存以重試。", "lang_not_saved": "未儲存", - "lang_download": "下載" + "lang_download": "下載", + "lang_staff_account": "員工帳戶", + "lang_api_integration": "API 整合", + "lang_allow_shared_phone_number": "允許共用電話號碼", + "lang_same_as_branch": "與分店相同", + "lang_15_character_gstin": "15位GSTIN", + "lang_customer_name_phone": "客戶姓名 / 電話" } diff --git a/tests/cloud-agent-bootstrap.test.js b/tests/cloud-agent-bootstrap.test.js index 0c19a68ad..a900bbd87 100644 --- a/tests/cloud-agent-bootstrap.test.js +++ b/tests/cloud-agent-bootstrap.test.js @@ -89,10 +89,10 @@ function connection(start, refreshCaptainDiscovery = async () => {}) { const helper = main.slice(main.indexOf('let cloudConnectionBusy ='), main.indexOf("ipcMain.handle('cloud:resume'")); const manager = { stop() {}, start }; const sandbox = { fs: { existsSync: () => false, readFileSync: () => { throw Object.assign(new Error('missing'), { code: 'ENOENT' }); }, writeFileSync() {}, chmodSync() {} }, validateActivation, path, app: { getPath: () => 'test' }, - process: { env: {} }, console: { log() {}, warn() {} }, CLOUD_CONFIG_FILE: 'test.json', syncAgentManager: manager, + __dirname: path.join(__dirname, '../src'), process: { env: {} }, console: { log() {}, warn() {} }, CLOUD_CONFIG_FILE: 'test.json', syncAgentManager: manager, WEBSITE_API: 'https://www.posnic.com', AbortSignal, fetch: async () => ({ ok: true, json: async () => ({ tenantDb: 'shop', branchIds: [] }) }), - require: (name) => name === 'mongodb' ? { MongoClient: class { + require: (name) => name.endsWith('supplier-email-index') ? { migrateSupplierEmailIndex: async () => {} } : name === 'mongodb' ? { MongoClient: class { async connect() {} async close() {} db() { return { collection: () => ({ find: () => ({ toArray: async () => [] }), countDocuments: async () => 0 }) }; } } } : require(name === './cloud-shop-identity' ? '../src/cloud-shop-identity' : name), diff --git a/tests/record-workspace.test.js b/tests/record-workspace.test.js index 013068044..40e215ad0 100644 --- a/tests/record-workspace.test.js +++ b/tests/record-workspace.test.js @@ -6,7 +6,7 @@ const { JSDOM } = require('jsdom'); const root = path.join(__dirname, '..'); const read = p => fs.readFileSync(path.join(root, p), 'utf8'); function fixture() { - const files = ['modals/user.html', 'modals/customer.html', 'modals/supplier.html', 'modals/branch.html', 'modals/category_add.html', 'modules/branchEdit.html', 'modules/settings_write.html']; + const files = ['modals/customer_category_add.html', 'modals/variant_add.html', 'modals/tax.html', 'modals/payment.html', 'modals/denomcash.html', 'modals/expense.html', 'modals/tableorder.html', 'modals/user.html', 'modals/customer.html', 'modals/supplier.html', 'modals/branch.html', 'modals/category_add.html', 'modules/branchEdit.html', 'modules/settings_write.html']; const dom = new JSDOM(files.map(p => read('frontend/' + p)).join('\n'), { runScripts: 'outside-only' }); const w = dom.window; w.eval(read('frontend/static/script/js/jquery.min.js')); @@ -102,3 +102,23 @@ for (const kind of ['customer', 'supplier', 'branch']) { dom.window.close(); }); } + +test('small editors and account choices preserve controls in their appropriate layouts', () => { + const {w,d,dom}=fixture(); + w.eval(read('frontend/static/script/js/core/record-workspace.js')); + w.PosnicPro.recordWorkspace.init(); + assert.ok(d.querySelector('#infobar-settings-sidebar-customercategory').classList.contains('record-dialog-small')); + assert.ok(d.querySelector('#apiMethod').closest('.record-account-types')); + assert.equal(d.querySelector('#customer_shared_phone').checked,false); + dom.window.close(); +}); +test('email validation accepts plus addressing and long domains', () => { + const {w,dom}=fixture(); + const methods={}; + w.jQuery.validator={addMethod:(name,fn)=>{methods[name]=fn;}}; + w.eval(read('frontend/static/script/js/validpassword-showhidepassword.js')); + const validate=value=>methods.emailExt.call({optional:()=>false},value,{}); + for(const value of ['shop+123@example.com','owner@example.technology']) assert.equal(validate(value),true); + for(const value of ['bad email@example.com','owner@@example.com','owner@example']) assert.equal(validate(value),false); + dom.window.close(); +});