Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/src/controllers/settings.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1260,7 +1260,7 @@ class SettingController extends BaseController {
async getTaxProfile(req, res) {
try {
const settingModel = this.createModelWithContext(req);
const result = await settingModel.getTaxProfileModel();
const result = await settingModel.getTaxProfileModel(req.query?.country);
const code = result.status ? 200 : 400;
return res.status(code).json({
type: result.status ? 'success' : 'error',
Expand Down
2 changes: 2 additions & 0 deletions api/src/models/branch.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,7 @@ class BranchModel {
*/
async createBranch(data, user = {}) {
try {
const regional = require('../services/branch-regional')(data);
// 1. Check duplicate
const duplicate = await this.model.findOne({
branch_name: { $regex: new RegExp(`^${data.name}$`, 'i') },
Expand Down Expand Up @@ -1218,6 +1219,7 @@ class BranchModel {
client_dateformat: 'dd/mm/yyyy',
server_dateformat: 'd/m/Y',
dateformat_text: '01/01/2018 -- dd/mm/yyyy',
...regional,
time_format: 'enable',
payment_gateway: [],
online_ordering: onlineOrdering.defaultConfig(),
Expand Down
11 changes: 10 additions & 1 deletion api/src/models/setting.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -3878,8 +3878,17 @@ class SettingModel extends BaseModel {
* is CALLED, how its registration number is labelled and shaped, how it
* displays. Presentation only - rates and math never travel here.
*/
async getTaxProfileModel() {
async getTaxProfileModel(country) {
try {
// Reference metadata only; this does not read another branch's settings.
if (typeof country === 'string' && country.trim()) {
const countries = require('../json/countries.json');
const entry = countries.countries.find((item) => item.value === country.trim());
const { code, profile } = require('../services/tax-profiles').profileForBranch({
sortname: entry?.sortname || '',
});
return { status: true, data: { code, country, registration: profile.registration } };
}
const branchCollection = await this.getCollection();
const branch = await branchCollection.findOne({
_id: this.normalizeId(this.branchId),
Expand Down
52 changes: 52 additions & 0 deletions api/src/services/branch-regional.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const { countries } = require('../json/countries.json');
const { currency } = require('../json/currency.json');
const currencyByCountry = require('../json/country_currency.json');
const taxProfiles = require('./tax-profiles');

// Whitelist regional fields; derived labels and symbols always come from reference data.
module.exports = function branchRegional(data) {
const country = countries.find(
(item) => item.value === data.country || item.sortname === data.country
);
const selected = data.currency_text
? currency.find((item) => item.value === data.currency_text)
: currency.find((item) => item.text === currencyByCountry[country?.sortname]);
if (data.currency_text && !selected) throw new Error('Choose a valid currency');
const unit = selected || currency.find((item) => item.text === 'INR');
const timezone = data.time_zone || (country?.sortname === 'IN' ? 'Asia/Kolkata' : 'UTC');
try {
new Intl.DateTimeFormat('en', { timeZone: timezone });
} catch (cause) {
throw new Error('Choose a valid time zone', { cause });
}
const formats = {
'dd/mm/yyyy': 'd/m/Y',
'yyyy/mm/dd': 'Y/m/d',
'mm/dd/yyyy': 'm/d/Y',
'D mm/dd': 'D m/d',
'MM dd yyyy': 'M d y',
'd MM yy': 'd M y',
'D d MM yy': 'D d M y',
};
const date = data.client_dateformat || 'dd/mm/yyyy';
if (!formats[date]) throw new Error('Choose a valid date format');
const tax = typeof data.branch_gstin_number === 'string' ? data.branch_gstin_number.trim() : '';
const { profile } = taxProfiles.profileForBranch({ sortname: country?.sortname });
if (tax.length > 100 || (tax && !taxProfiles.registrationValid(profile, tax))) {
throw new Error('Enter a valid ' + profile.registration.label);
}
return {
currency: unit.symbol,
currency_text: unit.value,
currency_type: data.currency_type === unit.text ? unit.text : unit.symbol,
currency_value: [{ currency_text: unit.text, currency_sign: unit.symbol }],
time_zone: timezone,
client_dateformat: date,
server_dateformat: formats[date],
dateformat_text: date,
branch_gstin_number: tax,
printing_address: (data.printing_address || data.address || '').trim().slice(0, 500),
website: typeof data.website === 'string' ? data.website.trim().slice(0, 50) : '',
pincode: typeof data.pincode === 'string' ? data.pincode.trim().slice(0, 15) : '',
};
};
2 changes: 2 additions & 0 deletions api/tests/unit/models/branch.model.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,8 @@ describe('BranchModel.createBranch', () => {
expect(r.status).toBe(true);
expect(r.data.insertId).toBe(branchId.toString());
expect(mockModel.create.mock.calls[0][0].city).toBe('Kochi');
expect(mockModel.create.mock.calls[0][0].branch_gstin_number).toBe('');
expect(mockModel.create.mock.calls[0][0].currency_value[0].currency_text).toBe('INR');
});

test('returns branchList array on success', async () => {
Expand Down
37 changes: 37 additions & 0 deletions api/tests/unit/services/branch-regional.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const regional = require('../../../src/services/branch-regional');
const { currency } = require('../../../src/json/currency.json');
describe('branch regional creation', () => {
test('blank tax number is optional in every supported country', () => {
for (const country of ['India', 'United Kingdom', 'Germany', 'United Arab Emirates'])
expect(regional({ country, branch_gstin_number: ' ' }).branch_gstin_number).toBe('');
});
test('country picks currency without defaulting international branches to INR', () => {
expect(regional({ country: 'United Kingdom' }).currency_value[0].currency_text).toBe('GBP');
expect(regional({ country: 'Germany' }).currency_value[0].currency_text).toBe('EUR');
});
test('explicit settings survive creation with canonical currency metadata', () => {
const usd = currency.find((x) => x.text === 'USD');
const result = regional({
country: 'United States',
currency_text: usd.value,
currency_type: 'USD',
time_zone: 'America/New_York',
client_dateformat: 'mm/dd/yyyy',
printing_address: ' Receipt address ',
pincode: 'SW1A 1AA',
});
expect(result).toMatchObject({
currency_type: 'USD',
time_zone: 'America/New_York',
client_dateformat: 'mm/dd/yyyy',
server_dateformat: 'm/d/Y',
printing_address: 'Receipt address',
pincode: 'SW1A 1AA',
});
});
test('invalid configured values fail before creating a branch', () => {
expect(() => regional({ country: 'India', branch_gstin_number: 'not-gstin' })).toThrow(/GST/);
expect(() => regional({ country: 'India', currency_text: 'invented' })).toThrow(/currency/);
expect(() => regional({ country: 'India', time_zone: 'not-a-zone' })).toThrow(/time zone/);
});
});
21 changes: 21 additions & 0 deletions docs/RECORD_FORM_COMPLETENESS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Record form completeness

New Unit uses the shared compact dialog. Supplier name/phone and user password/confirmation align in pairs. Missing user permissions are checked inside the user form and reveal the Access tab. Category shape previews use the selected swatch. Checkout settings have a readable maximum width.

New Branch exposes currency, currency display, timezone, date format, optional country-specific tax registration, printing address, website and postal code alongside the existing registers/sharing controls. The API derives currency metadata from reference data and validates regional values before creation. Branch tax validation is isolated from the India-only customer/supplier GST validators. The existing country profile registry remains the source of format rules.

State selection preserves saved values and ignores stale responses. No state is inferred from a city. The tested live shop has an empty saved state and needs its correct state saved on the branch.

## Data and synchronization

All regional values use existing fields on the existing tenant-scoped `branches` collection. Creation still uses BranchModel's existing identifiers, timestamps, user access update and seed/write path. The POS collection manifest, Gateway collection registry and agent config already include `branches` as a global-within-tenant batch lane. There are no new collections or credentials to replicate, no migration, and no change to conflict, deletion or attachment transport. Existing authorized tills receive these fields in the existing whole-document branch transfer. New web-created records become available to offline tills upon reconnection through that existing lane. No Gateway/agent rollout is required. Physical cross-till offline/reconnect delivery was not exercised in this web release; existing sync classification/protocol checks ran in the standard suite.

## Validation

- `npm run check`: all eight checks passed; 4,309 desktop tests passed, five skipped.
- Full API suite: 528 suites passed; 12,891 tests passed, 13 skipped.
- Focused form regressions: 12 passed.
- Branch model/regional focused suite: 173 passed.
- Frontend asset build passed.
- Offscreen browser previews covered light/dark themes, phone/select widgets and mobile overflow.
- Existing receipt/printer behavior is unchanged; standard desktop printer regression tests passed.
89 changes: 75 additions & 14 deletions frontend/modals/branch.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,81 @@ <h4 id="exampleModalCenterTitle"><span id="branch_title"><lang class="lang_nav_a
</a>
</div>
<div class="infobar-settings-sidebar-body">
<div class="custom-mode-setting loader-branch">
<form id='branch_add_form'>
<div class="custom-mode-setting loader-branch">
<form id='branch_add_form'>
<section id="branch_regional_fields"><h5><lang class="lang_record_regional">Regional & tax</lang></h5><div class="record-grid"><div class="form-group">
<label for="branch_currency">
<lang class="lang_currency_title">Currency </lang> <span class="text-danger">*</span>
</label>
<select class="form-control" name="currency_text" id="branch_currency" required=""></select>


</div><div class="form-group">
<label for="branch_currency_type">
<lang class="lang_choosesize_title">Currency Type</lang>
</label>
<select name="currency_type" id="branch_currency_type" class="form-control">
</select>
</div><div class="form-group">
<label for="branch_timezone">
<lang class="lang_timezone_title">Time Zone </lang> <span class="text-danger">*</span>
</label>
<select class="form-control" name="time_zone" id="branch_timezone" required=""></select>
</div><div class="form-group">
<label for="branch_dateformat">
<lang class="lang_dateformat_title">Date Format </lang><span class="text-danger">*</span>
</label>
<select class="form-control" id="branch_dateformat" name="client_dateformat" required="">
<option name="01/01/2018" value="dd/mm/yyyy" data-id="d/m/Y"
selected><lang class="lang_01_01_2018_dd_mm_yyyy">01/01/2018 - dd/mm/yyyy</lang></option>
<option name="2018/01/01" value="yyyy/mm/dd" data-id="Y/m/d">
<lang class="lang_2018_01_01_yyyy_mm_dd">2018/01/01 - yyyy/mm/dd</lang></option>
<option name="01/01/2018" value="mm/dd/yyyy" data-id="m/d/Y">
<lang class="lang_01_01_2018_mm_dd_yyyy">01/01/2018 - mm/dd/yyyy</lang></option>
<option name="Sun 01/18" value="D mm/dd" data-id="D m/d"><lang class="lang_sun_01_18_day_mm_dd">Sun 01/18 -
Day mm/dd</lang></option>
<option name="January 01 2018" value="MM dd yyyy" data-id="M d y">
<lang class="lang_january_01_2018_mm_dd_yyyy">January 01 2018 - MM dd yyyy</lang></option>
<option name="01 January 18" value="d MM yy" data-id="d M y"><lang class="lang_01_january_18_d_mm_yy">01
January 18 - d MM yy</lang></option>
<option name="Thu 01 January 18" value="D d MM yy"
data-id="D d M y"><lang class="lang_thu_01_january_18_day_d_mm_yy">Thu 01 January 18 - Day d MM yy</lang></option>
</select>


</div><div class="form-group">
<label for="branch_tax_number" data-toggle="tooltip" title="Gstrin number" data-t-title="lang_gstrin_number" class="label-text-color">
<lang class="lang_gstr_number">GSTIN Number</lang>
</label>
<input type="text" class="form-control" name="branch_gstin_number" id="branch_tax_number" maxlength="100" placeholder="Enter the GSTIN" data-t-placeholder="lang_enter_the_gstin" autocomplete="off">
</div><div class="form-group">
<label for="branch_printing_address">
<lang class="lang_printadd_title">Printing Address </lang>
</label>
<textarea name="printing_address" class="form-control" id="branch_printing_address" minlength="3" maxlength="500" placeholder="Enter the print address" data-t-placeholder="lang_enter_the_print_address"></textarea>
</div><div class="form-group">
<label for="branch_website">
<lang class="lang_website_title">Website </lang>
</label>
<input type="text" class="form-control" name="website" id="branch_website" minlength="3" maxlength="50" placeholder="Enter the website" data-t-placeholder="lang_enter_the_website" autocomplete="off">
</div><div class="form-group">
<label for="branch_pincode">
<lang class="lang_pincode_title">Pincode </lang>
</label>
<input type="text" class="form-control" name="pincode" id="branch_pincode" maxlength="15" placeholder="Enter the pincode" data-t-placeholder="lang_enter_the_pincode" autocomplete="off">
</div></div></section>
<div class="form-row">
<div class="form-group col-md-12">
<label for="name_branch">
<lang class="lang_name_title"> Name </lang>
<span class="text-danger">*</span>
<span class="text-danger">*</span>
</label>
<input type="text" class="form-control border-control branch-trigger" name="name" id="name_branch" minlength="3" maxlength="250" placeholder="Enter the name" data-t-placeholder="lang_enter_the_name" required>
<input type="hidden" name="id" id="branch_id">
<input type="hidden" name="logo" id="branch_logo">
</div>
</div>
<div class="form-row">
<div class="form-row">
<div class="form-group col-md-6">
<label for="branch_phone" class="control-label">
<lang class="lang_phone_title"> Phone </lang>
Expand All @@ -42,7 +103,7 @@ <h4 id="exampleModalCenterTitle"><span id="branch_title"><lang class="lang_nav_a
<lang class="lang_address_title"> Address </lang>
<span class="text-danger">*</span>
</label>
<textarea name="address" id="branch_address"
<textarea name="address" id="branch_address"
class="form-control border-control branch-trigger" minlength="3" maxlength="500" placeholder="Enter the address" data-t-placeholder="lang_enter_the_address"></textarea>
</div>
</div>
Expand All @@ -57,14 +118,14 @@ <h4 id="exampleModalCenterTitle"><span id="branch_title"><lang class="lang_nav_a
<div class="form-group col-md-6">
<label for="branch_state"><lang class="lang_state_title"> State </lang><span class="text-danger">*</span></label>
<select class="select2 setState" name="state" data-page="branch_state" id="branch_state" required></select>
</div>
</div>
</div>
</div>
<div class="form-row"><div class="form-group col-md-12"><label for="branch_city"><lang class="lang_city_title">City</lang></label><input type="text" id="branch_city" name="city" class="form-control border-control branch-trigger" maxlength="50"></div></div>
<div class="form-row">
<div class="form-row">
<div class="form-group col-8">
<label class="form-control-placeholder" for="register"><lang class="lang_addregister_title"> Add Register </lang> </label>
<div class="register-wrapper">
<div class="register-fields">
<div class="register-fields">
<div class="register-input">
<input type="text" id="register[0]" name="branchregister[0]" oninput="validate(this)" class="form-control border-control register_list branch-trigger" minlength="3" maxlength="20">
<span class="remove-field btn-round btn-danger-rgba"><i class="feather icon-trash"></i></span>
Expand Down Expand Up @@ -160,7 +221,7 @@ <h4 id="exampleModalCenterTitle"><lang class="lang_branchdetail_title"> Branch D
</a>
</div>
<div class="infobar-settings-sidebar-body">
<div class="custom-mode-setting loader-profile">
<div class="custom-mode-setting loader-profile">
<div class="slider-modal-body scroll_width">
<div class="row">
<div class="col-lg-6 col-sm-12" style="border-right:1px solid #ddd">
Expand Down Expand Up @@ -222,8 +283,8 @@ <h4 id="exampleModalCenterTitle"><lang class="lang_branchdetail_title"> Branch D
</table>
</div>
</div>


<div class="col-lg-6 col-sm-12" style="border-right:1px solid #ddd">
<div class="table-responsive">
<table class="table table-border table-striped mb-0">
Expand Down Expand Up @@ -277,8 +338,8 @@ <h4 id="exampleModalCenterTitle"><lang class="lang_branchdetail_title"> Branch D
</table>
</div>
</div>


</div>
</div>
</div>
Expand Down
Loading
Loading