mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +00:00
feat: integrate multiple branches and warehouses with import
This commit is contained in:
@@ -99,6 +99,7 @@ export interface IBillsFilter extends IDynamicListFilterDTO {
|
|||||||
stringifiedFilterRoles?: string;
|
stringifiedFilterRoles?: string;
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
filterQuery?: (q: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBillsService {
|
export interface IBillsService {
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ export interface ICreditNoteOpenedPayload {
|
|||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreditNotesQueryDTO {}
|
export interface ICreditNotesQueryDTO {
|
||||||
|
filterQuery?: (query: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICreditNotesQueryDTO extends IDynamicListFilter {
|
export interface ICreditNotesQueryDTO extends IDynamicListFilter {
|
||||||
page: number;
|
page: number;
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export interface IModelMetaFieldCommon2 {
|
|||||||
importHint?: string;
|
importHint?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
unique?: number;
|
unique?: number;
|
||||||
|
features?: Array<any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IModelMetaRelationField2 {
|
export interface IModelMetaRelationField2 {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface ISaleEstimateDTO {
|
|||||||
|
|
||||||
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {
|
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {
|
||||||
stringifiedFilterRoles?: string;
|
stringifiedFilterRoles?: string;
|
||||||
|
filterQuery?: (q: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISalesEstimatesService {
|
export interface ISalesEstimatesService {
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export interface ISalesInvoicesFilter extends IDynamicListFilter {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
searchKeyword?: string;
|
searchKeyword?: string;
|
||||||
|
filterQuery?: (q: Knex.QueryBuilder) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISalesInvoicesService {
|
export interface ISalesInvoicesService {
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export interface ISaleReceipt {
|
|||||||
entries?: IItemEntry[];
|
entries?: IItemEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISalesReceiptsFilter {}
|
export interface ISalesReceiptsFilter {
|
||||||
|
filterQuery?: (query: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ISaleReceiptDTO {
|
export interface ISaleReceiptDTO {
|
||||||
customerId: number;
|
customerId: number;
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export interface IVendorCreditsQueryDTO extends IDynamicListFilter {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
searchKeyword?: string;
|
searchKeyword?: string;
|
||||||
|
filterQuery?: (q: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVendorCreditEditingPayload {
|
export interface IVendorCreditEditingPayload {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Features } from '@/interfaces';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
defaultFilterField: 'vendor',
|
defaultFilterField: 'vendor',
|
||||||
defaultSort: {
|
defaultSort: {
|
||||||
@@ -167,6 +169,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branch: {
|
||||||
|
name: 'Branch',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'branch.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
|
warehouse: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'warehouse.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields2: {
|
fields2: {
|
||||||
billNumber: {
|
billNumber: {
|
||||||
@@ -238,6 +252,22 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
name: 'Branch',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Branch',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
warehouseId: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Warehouse',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.WAREHOUSES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -402,6 +402,7 @@ export default class Bill extends mixin(TenantModel, [
|
|||||||
const ItemEntry = require('models/ItemEntry');
|
const ItemEntry = require('models/ItemEntry');
|
||||||
const BillLandedCost = require('models/BillLandedCost');
|
const BillLandedCost = require('models/BillLandedCost');
|
||||||
const Branch = require('models/Branch');
|
const Branch = require('models/Branch');
|
||||||
|
const Warehouse = require('models/Warehouse');
|
||||||
const TaxRateTransaction = require('models/TaxRateTransaction');
|
const TaxRateTransaction = require('models/TaxRateTransaction');
|
||||||
const Document = require('models/Document');
|
const Document = require('models/Document');
|
||||||
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
|
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
|
||||||
@@ -453,6 +454,18 @@ export default class Bill extends mixin(TenantModel, [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bill may has associated warehouse.
|
||||||
|
*/
|
||||||
|
warehouse: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Warehouse.default,
|
||||||
|
join: {
|
||||||
|
from: 'bills.warehouseId',
|
||||||
|
to: 'warehouses.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bill may has associated tax rate transactions.
|
* Bill may has associated tax rate transactions.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Features } from '@/interfaces';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
defaultFilterField: 'vendor',
|
defaultFilterField: 'vendor',
|
||||||
defaultSort: {
|
defaultSort: {
|
||||||
@@ -141,6 +143,12 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branch: {
|
||||||
|
name: 'Branch',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'branch.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields2: {
|
fields2: {
|
||||||
vendorId: {
|
vendorId: {
|
||||||
@@ -204,5 +212,13 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
name: 'Branch',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Branch',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
11
packages/server/src/models/Branch.settings.ts
Normal file
11
packages/server/src/models/Branch.settings.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
fields2: {
|
||||||
|
name: {
|
||||||
|
name: 'Name',
|
||||||
|
fieldType: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: {},
|
||||||
|
fields: {}
|
||||||
|
};
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Model } from 'objection';
|
import { Model, mixin } from 'objection';
|
||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
|
import BranchMetadata from './Branch.settings';
|
||||||
|
import ModelSetting from './ModelSetting';
|
||||||
|
|
||||||
export default class Branch extends TenantModel {
|
export default class Branch extends mixin(TenantModel, [ModelSetting]) {
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
@@ -169,4 +171,11 @@ export default class Branch extends TenantModel {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model settings.
|
||||||
|
*/
|
||||||
|
static get meta() {
|
||||||
|
return BranchMetadata;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Features } from '@/interfaces';
|
||||||
|
|
||||||
function StatusFieldFilterQuery(query, role) {
|
function StatusFieldFilterQuery(query, role) {
|
||||||
query.modify('filterByStatus', role.value);
|
query.modify('filterByStatus', role.value);
|
||||||
}
|
}
|
||||||
@@ -100,7 +102,7 @@ export default {
|
|||||||
},
|
},
|
||||||
creditNoteDate: {
|
creditNoteDate: {
|
||||||
name: 'Credit Note Date',
|
name: 'Credit Note Date',
|
||||||
accessor: 'formattedCreditNoteDate'
|
accessor: 'formattedCreditNoteDate',
|
||||||
},
|
},
|
||||||
referenceNo: {
|
referenceNo: {
|
||||||
name: 'Reference No.',
|
name: 'Reference No.',
|
||||||
@@ -147,6 +149,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branch: {
|
||||||
|
name: 'Branch',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'branch.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
|
warehouse: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'warehouse.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields2: {
|
fields2: {
|
||||||
customerId: {
|
customerId: {
|
||||||
@@ -215,5 +229,21 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
name: 'Branch',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Branch',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
warehouseId: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Warehouse',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.WAREHOUSES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export default class CreditNote extends mixin(TenantModel, [
|
|||||||
const Customer = require('models/Customer');
|
const Customer = require('models/Customer');
|
||||||
const Branch = require('models/Branch');
|
const Branch = require('models/Branch');
|
||||||
const Document = require('models/Document');
|
const Document = require('models/Document');
|
||||||
|
const Warehouse = require('models/Warehouse');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/**
|
/**
|
||||||
@@ -235,6 +236,18 @@ export default class CreditNote extends mixin(TenantModel, [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit note may has associated warehouse.
|
||||||
|
*/
|
||||||
|
warehouse: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Warehouse.default,
|
||||||
|
join: {
|
||||||
|
from: 'credit_notes.warehouseId',
|
||||||
|
to: 'warehouses.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credit note may has many attached attachments.
|
* Credit note may has many attached attachments.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Features } from '@/interfaces';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
importable: true,
|
importable: true,
|
||||||
|
|
||||||
@@ -128,6 +130,12 @@ export default {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
printable: false,
|
printable: false,
|
||||||
},
|
},
|
||||||
|
branch: {
|
||||||
|
name: 'Branch',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'branch.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields2: {
|
fields2: {
|
||||||
customerId: {
|
customerId: {
|
||||||
@@ -189,5 +197,13 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
name: 'Branch',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Branch',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Features } from '@/interfaces';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
defaultFilterField: 'estimate_date',
|
defaultFilterField: 'estimate_date',
|
||||||
defaultSort: {
|
defaultSort: {
|
||||||
@@ -174,6 +176,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branch: {
|
||||||
|
name: 'Branch',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'branch.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
|
warehouse: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'warehouse.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields2: {
|
fields2: {
|
||||||
customerId: {
|
customerId: {
|
||||||
@@ -252,6 +266,22 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
name: 'Branch',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Branch',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
warehouseId: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Warehouse',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.WAREHOUSES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ export default class SaleEstimate extends mixin(TenantModel, [
|
|||||||
const ItemEntry = require('models/ItemEntry');
|
const ItemEntry = require('models/ItemEntry');
|
||||||
const Customer = require('models/Customer');
|
const Customer = require('models/Customer');
|
||||||
const Branch = require('models/Branch');
|
const Branch = require('models/Branch');
|
||||||
|
const Warehouse = require('models/Warehouse');
|
||||||
const Document = require('models/Document');
|
const Document = require('models/Document');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -221,6 +222,18 @@ export default class SaleEstimate extends mixin(TenantModel, [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sale estimate may has associated warehouse.
|
||||||
|
*/
|
||||||
|
warehouse: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Warehouse.default,
|
||||||
|
join: {
|
||||||
|
from: 'sales_estimates.warehouseId',
|
||||||
|
to: 'warehouses.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sale estimate transaction may has many attached attachments.
|
* Sale estimate transaction may has many attached attachments.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Features } from '@/interfaces';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
defaultFilterField: 'customer',
|
defaultFilterField: 'customer',
|
||||||
defaultSort: {
|
defaultSort: {
|
||||||
@@ -185,6 +187,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branch: {
|
||||||
|
name: 'Branch',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'branch.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
|
warehouse: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'warehouse.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields2: {
|
fields2: {
|
||||||
invoiceDate: {
|
invoiceDate: {
|
||||||
@@ -268,6 +282,22 @@ export default {
|
|||||||
fieldType: 'boolean',
|
fieldType: 'boolean',
|
||||||
printable: false,
|
printable: false,
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
name: 'Branch',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Branch',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
warehouseId: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Warehouse',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.WAREHOUSES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -408,6 +408,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
|||||||
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
|
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
|
||||||
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
|
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
|
||||||
const Branch = require('models/Branch');
|
const Branch = require('models/Branch');
|
||||||
|
const Warehouse = require('models/Warehouse');
|
||||||
const Account = require('models/Account');
|
const Account = require('models/Account');
|
||||||
const TaxRateTransaction = require('models/TaxRateTransaction');
|
const TaxRateTransaction = require('models/TaxRateTransaction');
|
||||||
const Document = require('models/Document');
|
const Document = require('models/Document');
|
||||||
@@ -499,6 +500,18 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice may has associated warehouse.
|
||||||
|
*/
|
||||||
|
warehouse: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Warehouse.default,
|
||||||
|
join: {
|
||||||
|
from: 'sales_invoices.warehouseId',
|
||||||
|
to: 'warehouses.id',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice may has associated written-off expense account.
|
* Invoice may has associated written-off expense account.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Features } from '@/interfaces';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
defaultFilterField: 'receipt_date',
|
defaultFilterField: 'receipt_date',
|
||||||
defaultSort: {
|
defaultSort: {
|
||||||
@@ -169,6 +171,18 @@ export default {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
printable: false,
|
printable: false,
|
||||||
},
|
},
|
||||||
|
branch: {
|
||||||
|
name: 'Branch',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'branch.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
|
warehouse: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'warehouse.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields2: {
|
fields2: {
|
||||||
receiptDate: {
|
receiptDate: {
|
||||||
@@ -245,6 +259,22 @@ export default {
|
|||||||
name: 'Receipt Message',
|
name: 'Receipt Message',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
name: 'Branch',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Branch',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
warehouseId: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Warehouse',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.WAREHOUSES],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export default class SaleReceipt extends mixin(TenantModel, [
|
|||||||
const ItemEntry = require('models/ItemEntry');
|
const ItemEntry = require('models/ItemEntry');
|
||||||
const Branch = require('models/Branch');
|
const Branch = require('models/Branch');
|
||||||
const Document = require('models/Document');
|
const Document = require('models/Document');
|
||||||
|
const Warehouse = require('models/Warehouse');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customer: {
|
customer: {
|
||||||
@@ -169,6 +170,18 @@ export default class SaleReceipt extends mixin(TenantModel, [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sale receipt may has associated warehouse.
|
||||||
|
*/
|
||||||
|
warehouse: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Warehouse.default,
|
||||||
|
join: {
|
||||||
|
from: 'sales_receipts.warehouseId',
|
||||||
|
to: 'warehouses.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sale receipt transaction may has many attached attachments.
|
* Sale receipt transaction may has many attached attachments.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Features } from '@/interfaces';
|
||||||
|
|
||||||
function StatusFieldFilterQuery(query, role) {
|
function StatusFieldFilterQuery(query, role) {
|
||||||
query.modify('filterByStatus', role.value);
|
query.modify('filterByStatus', role.value);
|
||||||
}
|
}
|
||||||
@@ -160,6 +162,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branch: {
|
||||||
|
name: 'Branch',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'branch.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
|
warehouse: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
type: 'text',
|
||||||
|
accessor: 'warehouse.name',
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields2: {
|
fields2: {
|
||||||
vendorId: {
|
vendorId: {
|
||||||
@@ -225,5 +239,21 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
branchId: {
|
||||||
|
name: 'Branch',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Branch',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.BRANCHES],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
warehouseId: {
|
||||||
|
name: 'Warehouse',
|
||||||
|
fieldType: 'relation',
|
||||||
|
relationModel: 'Warehouse',
|
||||||
|
relationImportMatch: ['name', 'code'],
|
||||||
|
features: [Features.WAREHOUSES],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export default class VendorCredit extends mixin(TenantModel, [
|
|||||||
const ItemEntry = require('models/ItemEntry');
|
const ItemEntry = require('models/ItemEntry');
|
||||||
const Branch = require('models/Branch');
|
const Branch = require('models/Branch');
|
||||||
const Document = require('models/Document');
|
const Document = require('models/Document');
|
||||||
|
const Warehouse = require('models/Warehouse');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
vendor: {
|
vendor: {
|
||||||
@@ -217,6 +218,18 @@ export default class VendorCredit extends mixin(TenantModel, [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor credit may has associated warehouse.
|
||||||
|
*/
|
||||||
|
warehouse: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Warehouse.default,
|
||||||
|
join: {
|
||||||
|
from: 'vendor_credits.warehouseId',
|
||||||
|
to: 'warehouses.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vendor credit may has many attached attachments.
|
* Vendor credit may has many attached attachments.
|
||||||
*/
|
*/
|
||||||
|
|||||||
11
packages/server/src/models/Warehouse.settings.ts
Normal file
11
packages/server/src/models/Warehouse.settings.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
fields2: {
|
||||||
|
name: {
|
||||||
|
name: 'Name',
|
||||||
|
fieldType: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: {},
|
||||||
|
fields: {}
|
||||||
|
};
|
||||||
@@ -15,12 +15,17 @@ export class CreditNotesExportable extends Exportable {
|
|||||||
* @returns {}
|
* @returns {}
|
||||||
*/
|
*/
|
||||||
public exportable(tenantId: number, query: ICreditNotesQueryDTO) {
|
public exportable(tenantId: number, query: ICreditNotesQueryDTO) {
|
||||||
|
const filterQuery = (query) => {
|
||||||
|
query.withGraphFetched('branch');
|
||||||
|
query.withGraphFetched('warehouse');
|
||||||
|
};
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12000,
|
pageSize: 12000,
|
||||||
|
filterQuery,
|
||||||
} as ICreditNotesQueryDTO;
|
} as ICreditNotesQueryDTO;
|
||||||
|
|
||||||
return this.getCreditNotes
|
return this.getCreditNotes
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export default class ListCreditNotes extends BaseCreditNotes {
|
|||||||
builder.withGraphFetched('entries.item');
|
builder.withGraphFetched('entries.item');
|
||||||
builder.withGraphFetched('customer');
|
builder.withGraphFetched('customer');
|
||||||
dynamicFilter.buildQuery()(builder);
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
creditNotesQuery?.filterQuery && creditNotesQuery?.filterQuery(builder);
|
||||||
})
|
})
|
||||||
.pagination(filter.page - 1, filter.pageSize);
|
.pagination(filter.page - 1, filter.pageSize);
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ export class ExportResourceService {
|
|||||||
) {
|
) {
|
||||||
const resource = sanitizeResourceName(resourceName);
|
const resource = sanitizeResourceName(resourceName);
|
||||||
const resourceMeta = this.getResourceMeta(tenantId, resource);
|
const resourceMeta = this.getResourceMeta(tenantId, resource);
|
||||||
|
const resourceColumns = this.resourceService.getResourceColumns(
|
||||||
|
tenantId,
|
||||||
|
resource
|
||||||
|
);
|
||||||
this.validateResourceMeta(resourceMeta);
|
this.validateResourceMeta(resourceMeta);
|
||||||
|
|
||||||
const data = await this.getExportableData(tenantId, resource);
|
const data = await this.getExportableData(tenantId, resource);
|
||||||
@@ -64,7 +67,7 @@ export class ExportResourceService {
|
|||||||
|
|
||||||
// Returns the csv, xlsx format.
|
// Returns the csv, xlsx format.
|
||||||
if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) {
|
if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) {
|
||||||
const exportableColumns = this.getExportableColumns(resourceMeta);
|
const exportableColumns = this.getExportableColumns(resourceColumns);
|
||||||
const workbook = this.createWorkbook(transformed, exportableColumns);
|
const workbook = this.createWorkbook(transformed, exportableColumns);
|
||||||
|
|
||||||
return this.exportWorkbook(workbook, format);
|
return this.exportWorkbook(workbook, format);
|
||||||
@@ -143,7 +146,7 @@ export class ExportResourceService {
|
|||||||
* @param {IModelMeta} resourceMeta - The metadata of the resource.
|
* @param {IModelMeta} resourceMeta - The metadata of the resource.
|
||||||
* @returns An array of exportable columns.
|
* @returns An array of exportable columns.
|
||||||
*/
|
*/
|
||||||
private getExportableColumns(resourceMeta: IModelMeta) {
|
private getExportableColumns(resourceColumns: any) {
|
||||||
const processColumns = (
|
const processColumns = (
|
||||||
columns: { [key: string]: IModelMetaColumn },
|
columns: { [key: string]: IModelMetaColumn },
|
||||||
parent = ''
|
parent = ''
|
||||||
@@ -166,7 +169,7 @@ export class ExportResourceService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return processColumns(resourceMeta.columns);
|
return processColumns(resourceColumns);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPrintableColumns(resourceMeta: IModelMeta) {
|
private getPrintableColumns(resourceMeta: IModelMeta) {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export class ImportFileMapping {
|
|||||||
// Invalidate the from/to map attributes.
|
// Invalidate the from/to map attributes.
|
||||||
this.validateMapsAttrs(tenantId, importFile, maps);
|
this.validateMapsAttrs(tenantId, importFile, maps);
|
||||||
|
|
||||||
|
// @todo validate the required fields.
|
||||||
|
|
||||||
// Validate the diplicated relations of map attrs.
|
// Validate the diplicated relations of map attrs.
|
||||||
this.validateDuplicatedMapAttrs(maps);
|
this.validateDuplicatedMapAttrs(maps);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export class BillPaymentExportable extends Exportable {
|
|||||||
public exportable(tenantId: number, query: any) {
|
public exportable(tenantId: number, query: any) {
|
||||||
const filterQuery = (builder) => {
|
const filterQuery = (builder) => {
|
||||||
builder.withGraphFetched('entries.bill');
|
builder.withGraphFetched('entries.bill');
|
||||||
|
builder.withGraphFetched('branch');
|
||||||
};
|
};
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { Knex } from 'knex';
|
||||||
import { IBillsFilter } from '@/interfaces';
|
import { IBillsFilter } from '@/interfaces';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
import { BillsApplication } from './BillsApplication';
|
import { BillsApplication } from './BillsApplication';
|
||||||
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
import Objection from 'objection';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BillsExportable extends Exportable {
|
export class BillsExportable extends Exportable {
|
||||||
@@ -15,12 +17,17 @@ export class BillsExportable extends Exportable {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public exportable(tenantId: number, query: IBillsFilter) {
|
public exportable(tenantId: number, query: IBillsFilter) {
|
||||||
|
const filterQuery = (query) => {
|
||||||
|
query.withGraphFetched('branch');
|
||||||
|
query.withGraphFetched('warehouse');
|
||||||
|
};
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: EXPORT_SIZE_LIMIT,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
|
filterQuery,
|
||||||
} as IBillsFilter;
|
} as IBillsFilter;
|
||||||
|
|
||||||
return this.billsApplication
|
return this.billsApplication
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ export class GetBills {
|
|||||||
builder.withGraphFetched('vendor');
|
builder.withGraphFetched('vendor');
|
||||||
builder.withGraphFetched('entries.item');
|
builder.withGraphFetched('entries.item');
|
||||||
dynamicFilter.buildQuery()(builder);
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
|
||||||
|
// Filter query.
|
||||||
|
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
|
||||||
})
|
})
|
||||||
.pagination(filter.page - 1, filter.pageSize);
|
.pagination(filter.page - 1, filter.pageSize);
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ export default class ListVendorCredits extends BaseVendorCredit {
|
|||||||
builder.withGraphFetched('entries');
|
builder.withGraphFetched('entries');
|
||||||
builder.withGraphFetched('vendor');
|
builder.withGraphFetched('vendor');
|
||||||
dynamicFilter.buildQuery()(builder);
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
|
||||||
|
// Gives ability to inject custom query to filter results.
|
||||||
|
vendorCreditQuery?.filterQuery &&
|
||||||
|
vendorCreditQuery?.filterQuery(builder);
|
||||||
})
|
})
|
||||||
.pagination(filter.page - 1, filter.pageSize);
|
.pagination(filter.page - 1, filter.pageSize);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { IVendorCreditsQueryDTO } from '@/interfaces';
|
import { IVendorCreditsQueryDTO } from '@/interfaces';
|
||||||
import ListVendorCredits from './ListVendorCredits';
|
import ListVendorCredits from './ListVendorCredits';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
|
import { QueryBuilder } from 'knex';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class VendorCreditsExportable extends Exportable {
|
export class VendorCreditsExportable extends Exportable {
|
||||||
@@ -15,12 +16,17 @@ export class VendorCreditsExportable extends Exportable {
|
|||||||
* @returns {}
|
* @returns {}
|
||||||
*/
|
*/
|
||||||
public exportable(tenantId: number, query: IVendorCreditsQueryDTO) {
|
public exportable(tenantId: number, query: IVendorCreditsQueryDTO) {
|
||||||
|
const filterQuery = (query) => {
|
||||||
|
query.withGraphFetched('branch');
|
||||||
|
query.withGraphFetched('warehouse');
|
||||||
|
};
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12000,
|
pageSize: 12000,
|
||||||
|
filterQuery,
|
||||||
} as IVendorCreditsQueryDTO;
|
} as IVendorCreditsQueryDTO;
|
||||||
|
|
||||||
return this.getVendorCredits
|
return this.getVendorCredits
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export class GetSaleEstimates {
|
|||||||
builder.withGraphFetched('entries');
|
builder.withGraphFetched('entries');
|
||||||
builder.withGraphFetched('entries.item');
|
builder.withGraphFetched('entries.item');
|
||||||
dynamicFilter.buildQuery()(builder);
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
|
||||||
})
|
})
|
||||||
.pagination(filter.page - 1, filter.pageSize);
|
.pagination(filter.page - 1, filter.pageSize);
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ export class SaleEstimatesExportable extends Exportable {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public exportable(tenantId: number, query: ISalesInvoicesFilter) {
|
public exportable(tenantId: number, query: ISalesInvoicesFilter) {
|
||||||
|
const filterQuery = (query) => {
|
||||||
|
query.withGraphFetched('branch');
|
||||||
|
query.withGraphFetched('warehouse');
|
||||||
|
};
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: EXPORT_SIZE_LIMIT,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
|
filterQuery,
|
||||||
} as ISalesInvoicesFilter;
|
} as ISalesInvoicesFilter;
|
||||||
|
|
||||||
return this.saleEstimatesApplication
|
return this.saleEstimatesApplication
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export class GetSaleInvoices {
|
|||||||
builder.withGraphFetched('entries.item');
|
builder.withGraphFetched('entries.item');
|
||||||
builder.withGraphFetched('customer');
|
builder.withGraphFetched('customer');
|
||||||
dynamicFilter.buildQuery()(builder);
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
|
||||||
})
|
})
|
||||||
.pagination(filter.page - 1, filter.pageSize);
|
.pagination(filter.page - 1, filter.pageSize);
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ export class SaleInvoicesExportable extends Exportable {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public exportable(tenantId: number, query: ISalesInvoicesFilter) {
|
public exportable(tenantId: number, query: ISalesInvoicesFilter) {
|
||||||
|
const filterQuery = (query) => {
|
||||||
|
query.withGraphFetched('branch');
|
||||||
|
query.withGraphFetched('warehouse');
|
||||||
|
};
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: EXPORT_SIZE_LIMIT,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
|
filterQuery,
|
||||||
} as ISalesInvoicesFilter;
|
} as ISalesInvoicesFilter;
|
||||||
|
|
||||||
return this.saleInvoicesApplication
|
return this.saleInvoicesApplication
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export class PaymentsReceivedExportable extends Exportable {
|
|||||||
public exportable(tenantId: number, query: IPaymentsReceivedFilter) {
|
public exportable(tenantId: number, query: IPaymentsReceivedFilter) {
|
||||||
const filterQuery = (builder) => {
|
const filterQuery = (builder) => {
|
||||||
builder.withGraphFetched('entries.invoice');
|
builder.withGraphFetched('entries.invoice');
|
||||||
|
builder.withGraphFetched('branch');
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export class GetSaleReceipts {
|
|||||||
builder.withGraphFetched('entries.item');
|
builder.withGraphFetched('entries.item');
|
||||||
|
|
||||||
dynamicFilter.buildQuery()(builder);
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
|
||||||
})
|
})
|
||||||
.pagination(filter.page - 1, filter.pageSize);
|
.pagination(filter.page - 1, filter.pageSize);
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ export class SaleReceiptsExportable extends Exportable {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public exportable(tenantId: number, query: ISalesReceiptsFilter) {
|
public exportable(tenantId: number, query: ISalesReceiptsFilter) {
|
||||||
|
const filterQuery = (query) => {
|
||||||
|
query.withGraphFetched('branch');
|
||||||
|
query.withGraphFetched('warehouse');
|
||||||
|
};
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: EXPORT_SIZE_LIMIT,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
|
filterQuery,
|
||||||
} as ISalesReceiptsFilter;
|
} as ISalesReceiptsFilter;
|
||||||
|
|
||||||
return this.saleReceiptsApp
|
return this.saleReceiptsApp
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ export default function ReceiptsImport() {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const handleCancelBtnClick = () => {
|
const handleCancelBtnClick = () => {
|
||||||
history.push('/accounts');
|
history.push('/receipts');
|
||||||
};
|
};
|
||||||
const handleImportSuccess = () => {
|
const handleImportSuccess = () => {
|
||||||
history.push('/accounts');
|
history.push('/receipts');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user