Compare commits

..

5 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
5dd7179cca Merge pull request #999 from bigcapitalhq/fix/attachments-for-all-transactions
fix(server): add attachment support for all transaction types
2026-02-26 23:18:41 +02:00
Ahmed Bouhuolia
3575d54efa fix: add attachment support for all transaction types
Fixed attachments not showing in edit forms for various transaction types by:

1. Adding @InjectAttachable() decorator to models:
   - SaleReceipt, SaleEstimate, CreditNote, PaymentReceived
   - Bill, BillPayment, VendorCredit
   - ManualJournal, Expense

2. Fixing transformers to include attachments in API responses:
   - SaleReceiptTransformer, PaymentReceivedTransformer

3. Registering missing event subscribers in Attachment.module.ts:
   - AttachmentsOnSaleReceipts, AttachmentsOnSaleEstimates

4. Fixing DocumentLink model relation mapping:
   - Changed Document.default to Document for proper module export

5. Fixing PaymentReceived model_ref consistency:
   - Changed from 'PaymentReceive' to 'PaymentReceived' to match class name

6. Adding missing withGraphFetched('attachments') to GetPaymentReceived.service.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 23:16:12 +02:00
Ahmed Bouhuolia
6991ec7780 Merge pull request #997 from bigcapitalhq/fix/model-timestamps-missing-columns
fix(models): remove timestamps from models where tables lack createdAt/updatedAt columns
2026-02-26 05:59:49 +02:00
Ahmed Bouhuolia
ad252d2e4a fix(models): remove timestamps from models where tables lack createdAt/updatedAt columns
Add withDateSessionMixin for proper timestamp handling and fix models
to return empty timestamps array when database tables don't have
created_at/updated_at columns. This prevents ORM insert/update errors.

Models updated:
- Branch, Role, RolePermission, ViewColumn, ViewRole
- InventoryAdjustment, InventoryAdjustmentEntry, InventoryTransactionMeta
- BillLandedCostEntry, CreditNote, CreditNoteAppliedInvoice, RefundCreditNote
- PaymentReceived, SaleInvoice, SaleReceipt, Item, ItemEntry
- RefundVendorCredit, VendorCreditAppliedBill
- ItemWarehouseQuantity, Warehouse, WarehouseTransfer, WarehouseTransferEntry
- Setting, TenantMetadataModel, TenantUser

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 05:57:55 +02:00
Ahmed Bouhuolia
12eb8c32dc Merge pull request #996 from bigcapitalhq/fix/account-type-not-selected-banking-edit
fix(webapp): account type not pre-selected when editing from banking page
2026-02-25 22:12:38 +02:00
40 changed files with 194 additions and 33 deletions

View File

@@ -1,5 +1,6 @@
import { QueryBuilder, Model } from 'objection';
import { QueryBuilder, Model, mixin } from 'objection';
import { ModelHasRelationsError } from '@/common/exceptions/ModelHasRelations.exception';
import { withDateSessionMixin } from './withDateSessionMixin';
interface PaginationResult<M extends Model> {
results: M[];
@@ -69,6 +70,7 @@ export class PaginationQueryBuilder<
dependentRelationNames.forEach((relationName: string) => {
recordQuery.withGraphFetched(relationName);
});
const record = await recordQuery;
const hasRelations = dependentRelationNames.some((name) => {
@@ -97,7 +99,7 @@ export class BaseQueryBuilder<
}
}
export class BaseModel extends Model {
export class BaseModel extends mixin(Model, [withDateSessionMixin]) {
public readonly id: number;
public readonly tableName: string;

View File

@@ -0,0 +1,40 @@
import * as moment from 'moment';
import { Model } from 'objection';
type Constructor<T = {}> = new (...args: any[]) => T;
export const withDateSessionMixin = <T extends Constructor<Model>>(BaseModel: T) => {
return class DateSession extends BaseModel {
constructor(...args: any[]) {
super(...args);
}
get timestamps() {
return [];
}
$beforeUpdate(opt, context) {
const maybePromise = super.$beforeUpdate(opt, context);
return Promise.resolve(maybePromise).then(() => {
const key = this.timestamps[1];
if (key && !this[key]) {
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
}
});
}
$beforeInsert(context) {
const maybePromise = super.$beforeInsert(context);
return Promise.resolve(maybePromise).then(() => {
const key = this.timestamps[0];
if (key && !this[key]) {
this[key] = moment().format('YYYY/MM/DD HH:mm:ss');
}
});
}
}
}

View File

@@ -15,6 +15,8 @@ import { AttachmentsOnPaymentsReceived } from "./events/AttachmentsOnPaymentsRec
import { AttachmentsOnManualJournals } from "./events/AttachmentsOnManualJournals";
import { AttachmentsOnVendorCredits } from "./events/AttachmentsOnVendorCredits";
import { AttachmentsOnSaleInvoiceCreated } from "./events/AttachmentsOnSaleInvoice";
import { AttachmentsOnSaleReceipt } from "./events/AttachmentsOnSaleReceipts";
import { AttachmentsOnSaleEstimates } from "./events/AttachmentsOnSaleEstimates";
import { AttachmentsController } from "./Attachments.controller";
import { RegisterTenancyModel } from "../Tenancy/TenancyModels/Tenancy.module";
import { DocumentModel } from "./models/Document.model";
@@ -50,6 +52,8 @@ const models = [
AttachmentsOnManualJournals,
AttachmentsOnVendorCredits,
AttachmentsOnSaleInvoiceCreated,
AttachmentsOnSaleReceipt,
AttachmentsOnSaleEstimates,
AttachmentsApplication,
UploadDocument,
AttachmentUploadPipeline,

View File

@@ -55,7 +55,7 @@ export class AttachmentsOnPaymentsReceived {
);
await this.linkAttachmentService.bulkLink(
keys,
'PaymentReceive',
'PaymentReceived',
paymentReceive.id,
trx,
);
@@ -76,7 +76,7 @@ export class AttachmentsOnPaymentsReceived {
);
await this.unlinkAttachmentService.unlinkUnpresentedKeys(
keys,
'PaymentReceive',
'PaymentReceived',
oldPaymentReceive.id,
trx,
);
@@ -100,7 +100,7 @@ export class AttachmentsOnPaymentsReceived {
);
await this.linkAttachmentService.bulkLink(
keys,
'PaymentReceive',
'PaymentReceived',
oldPaymentReceive.id,
trx,
);
@@ -117,7 +117,7 @@ export class AttachmentsOnPaymentsReceived {
trx,
}: IPaymentReceivedDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys(
'PaymentReceive',
'PaymentReceived',
oldPaymentReceive.id,
trx,
);

View File

@@ -13,6 +13,13 @@ export class BillLandedCostEntry extends BaseModel {
return 'bill_located_cost_entries';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/

View File

@@ -9,7 +9,9 @@ import { BillPaymentMeta } from './BillPayment.meta';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { BillPaymentDefaultViews } from '../constants';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
@ImportableModel()
@ExportableModel()
@InjectModelMeta(BillPaymentMeta)

View File

@@ -13,7 +13,9 @@ import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/Inje
import { BillMeta } from './Bill.meta';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { BillDefaultViews } from '../Bills.constants';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
@ExportableModel()
@InjectModelMeta(BillMeta)
@InjectModelDefaultViews(BillDefaultViews)

View File

@@ -29,7 +29,7 @@ export class Branch extends BaseModel{
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -28,7 +28,7 @@ export class DocumentLink extends BaseModel{
* Relationship mapping.
*/
static get relationMappings() {
const Document = require('./Document');
const { Document } = require('./Document');
return {
/**
@@ -36,7 +36,7 @@ export class DocumentLink extends BaseModel{
*/
document: {
relation: Model.HasOneRelation,
modelClass: Document.default,
modelClass: Document,
join: {
from: 'document_links.documentId',
to: 'documents.id',

View File

@@ -34,7 +34,7 @@ export class RefundCreditNote extends BaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -11,7 +11,9 @@ import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
import { CreditNoteMeta } from './CreditNote.meta';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { CreditNoteDefaultViews } from '../constants';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
@ExportableModel()
@ImportableModel()
@InjectModelMeta(CreditNoteMeta)
@@ -56,7 +58,7 @@ export class CreditNote extends TenantBaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -26,7 +26,7 @@ export class CreditNoteAppliedInvoice extends BaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -9,7 +9,9 @@ import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/Inje
import { ExpenseMeta } from './Expense.meta';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { ExpenseDefaultViews } from '../constants';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
@ExportableModel()
@ImportableModel()
@InjectModelMeta(ExpenseMeta)

View File

@@ -32,7 +32,7 @@ export class InventoryAdjustment extends TenantBaseModel {
* Timestamps columns.
*/
get timestamps(): Array<string> {
return ['created_at'];
return ['createdAt'];
}
/**

View File

@@ -20,6 +20,13 @@ export class InventoryAdjustmentEntry extends BaseModel {
return 'inventory_adjustments_entries';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/

View File

@@ -13,6 +13,13 @@ export class InventoryTransactionMeta extends BaseModel {
return 'inventory_transaction_meta';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/

View File

@@ -44,6 +44,13 @@ export class Item extends TenantBaseModel {
return 'items';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Model modifiers.
*/

View File

@@ -8,7 +8,9 @@ import { ManualJournalMeta } from './ManualJournal.meta';
import { ImportableModel } from '@/modules/Import/decorators/Import.decorator';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { ManualJournalDefaultViews } from '../constants';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
@ExportableModel()
@ImportableModel()
@InjectModelMeta(ManualJournalMeta)

View File

@@ -1,5 +1,6 @@
import { Model } from 'objection';
import { PaymentReceivedEntry } from './PaymentReceivedEntry';
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator';
import { ImportableModel } from '@/modules/Import/decorators/Import.decorator';
@@ -7,7 +8,9 @@ import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/Inje
import { PaymentReceivedMeta } from './PaymentReceived.meta';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { PaymentReceivedDefaultViews } from '../constants';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
@ExportableModel()
@ImportableModel()
@InjectModelMeta(PaymentReceivedMeta)
@@ -31,6 +34,7 @@ export class PaymentReceived extends TenantBaseModel {
updatedAt: string;
entries?: PaymentReceivedEntry[];
public attachments!: Document[];
/**
* Table name.
@@ -43,7 +47,7 @@ export class PaymentReceived extends TenantBaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**
@@ -159,7 +163,7 @@ export class PaymentReceived extends TenantBaseModel {
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'PaymentReceive');
query.where('model_ref', 'PaymentReceived');
},
},

View File

@@ -32,6 +32,7 @@ export class GetPaymentReceivedService {
.withGraphFetched('entries.invoice')
.withGraphFetched('transactions')
.withGraphFetched('branch')
.withGraphFetched('attachments')
.findById(paymentReceiveId);
if (!paymentReceive) {

View File

@@ -2,6 +2,7 @@ import { Transformer } from '../../Transformer/Transformer';
import { PaymentReceived } from '../models/PaymentReceived';
import { PaymentReceivedEntry } from '../models/PaymentReceivedEntry';
import { PaymentReceivedEntryTransfromer } from './PaymentReceivedEntryTransformer';
import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer';
export class PaymentReceiveTransfromer extends Transformer {
/**
@@ -17,6 +18,7 @@ export class PaymentReceiveTransfromer extends Transformer {
'formattedAmount',
'formattedExchangeRate',
'entries',
'attachments',
];
};
@@ -89,4 +91,13 @@ export class PaymentReceiveTransfromer extends Transformer {
protected entries = (payment: PaymentReceived): PaymentReceivedEntry[] => {
return this.item(payment.entries, new PaymentReceivedEntryTransfromer());
};
/**
* Retrieves the payment received attachments.
* @param {PaymentReceived} payment
* @returns
*/
protected attachments = (payment: PaymentReceived) => {
return this.item(payment.attachments, new AttachmentTransformer());
};
}

View File

@@ -16,6 +16,13 @@ export class Role extends TenantBaseModel {
return 'roles';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/

View File

@@ -13,6 +13,13 @@ export class RolePermission extends TenantBaseModel {
return 'role_permissions';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/

View File

@@ -12,7 +12,9 @@ import { Customer } from '@/modules/Customers/models/Customer';
import { DiscountType } from '@/common/types/Discount';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { SaleEstimateDefaultViews } from '../constants';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
@ExportableModel()
@ImportableModel()
@InjectModelMeta(SaleEstimateMeta)

View File

@@ -74,7 +74,7 @@ export class SaleInvoice extends TenantBaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -5,6 +5,7 @@ import { BaseModel } from '@/models/Model';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { Customer } from '@/modules/Customers/models/Customer';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
import { Branch } from '@/modules/Branches/models/Branch.model';
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
import { DiscountType } from '@/common/types/Discount';
@@ -15,6 +16,7 @@ import { SearchableBaseModelMixin } from '@/modules/DynamicListing/models/Search
import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator';
import { ImportableModel } from '@/modules/Import/decorators/Import.decorator';
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
import { SaleReceiptMeta } from './SaleReceipt.meta';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { SaleReceiptDefaultViews } from '../constants';
@@ -26,6 +28,7 @@ const ExtendedModel = R.pipe(
MetadataModelMixin,
)(BaseModel);
@InjectAttachable()
@ExportableModel()
@ImportableModel()
@InjectModelMeta(SaleReceiptMeta)
@@ -58,6 +61,7 @@ export class SaleReceipt extends ExtendedModel {
public customer!: Customer;
public entries!: ItemEntry[];
public transactions!: AccountTransaction[];
public attachments!: Document[];
public branch!: Branch;
public warehouse!: Warehouse;
@@ -72,7 +76,7 @@ export class SaleReceipt extends ExtendedModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -139,22 +139,22 @@ export class SaleReceiptTransformer extends Transformer {
};
/**
* Retrieves the entries of the credit note.
* @param {ISaleReceipt} credit
* Retrieves the entries of the sale receipt.
* @param {ISaleReceipt} receipt
* @returns {}
*/
// protected entries = (receipt: SaleReceipt) => {
// return this.item(receipt.entries, new ItemEntryTransformer(), {
// currencyCode: receipt.currencyCode,
// });
// };
protected entries = (receipt: SaleReceipt) => {
return this.item(receipt.entries, new ItemEntryTransformer(), {
currencyCode: receipt.currencyCode,
});
};
/**
* Retrieves the sale receipt attachments.
* @param {SaleReceipt} receipt
* @returns
*/
// protected attachments = (receipt: SaleReceipt) => {
// return this.item(receipt.attachments, new AttachmentTransformer());
// };
protected attachments = (receipt: SaleReceipt) => {
return this.item(receipt.attachments, new AttachmentTransformer());
};
}

View File

@@ -10,6 +10,10 @@ export class Setting extends BaseModel {
return 'settings';
}
get timestamps() {
return [];
}
/**
* Extra metadata query to query with the current authenticate user.
* @param {Object} query

View File

@@ -50,6 +50,13 @@ export class TenantMetadata extends BaseModel {
*/
static tableName = 'tenants_metadata';
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Virtual attributes.
*/

View File

@@ -24,7 +24,7 @@ export class TenantUser extends TenantBaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -53,7 +53,7 @@ export class ItemEntry extends BaseModel {
* @returns {string[]}
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -11,7 +11,9 @@ import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/Inje
import { VendorCreditMeta } from './VendorCredit.meta';
import { InjectModelDefaultViews } from '@/modules/Views/decorators/InjectModelDefaultViews.decorator';
import { VendorCreditDefaultViews } from '../constants';
import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator';
@InjectAttachable()
@ExportableModel()
@ImportableModel()
@InjectModelMeta(VendorCreditMeta)

View File

@@ -26,7 +26,7 @@ export class VendorCreditAppliedBill extends BaseModel {
* Timestamps columns.
*/
public get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -32,7 +32,7 @@ export class RefundVendorCredit extends BaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/*

View File

@@ -8,6 +8,13 @@ export class ViewColumn extends BaseModel {
return 'view_has_columns';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/

View File

@@ -25,6 +25,13 @@ export class ViewRole extends BaseModel {
return 'view_roles';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/

View File

@@ -9,6 +9,13 @@ export class ItemWarehouseQuantity extends BaseModel{
return 'items_warehouses_quantity';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Relation mappings.
*/

View File

@@ -24,7 +24,7 @@ export class Warehouse extends BaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -28,7 +28,7 @@ export class WarehouseTransfer extends TenantBaseModel {
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
return ['createdAt', 'updatedAt'];
}
/**

View File

@@ -19,6 +19,13 @@ export class WarehouseTransferEntry extends TenantBaseModel {
return 'warehouses_transfers_entries';
}
/**
* Timestamps columns.
*/
get timestamps() {
return [];
}
/**
* Virtual attributes.
*/