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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct JournalSetCreateCommand: MutatingCommand {
static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a journal set",
discussion: """
The entries must balance: their debit values, negative for credits, add up to zero. User categories \
such as 901 and 907 need a user, and capital asset categories 601 to 607 a capital asset type. \
Categories, users and other records take a URL or an ID.

Example:
--entry '{"category": "907", "user": "1", "debit_value": 250}'
--entry '{
"category": "999",
"debit_value": -250,
"description": "Suspense, to clear"
}'
"""
)

@Option(name: .long, help: "Date of the entries (YYYY-MM-DD)")
var datedOn: String

@Option(name: .long, help: "Description")
var description: String

@Option(name: .long, help: "Tag identifying sets your tool creates - tagged sets are read-only in FreeAgent")
var tag: String?

@Option(name: .long, help: "An entry as a JSON object of the API's entry fields, repeated for each entry")
var entry: [Components.Schemas.JournalEntryPayload]

@Flag(help: "Print the request instead of sending it")
var dryRun = false

@Flag(name: .long, help: "Output JSON")
var json = false

func perform(client: Client) async throws -> Components.Schemas.JournalSetResponse {
let journalSetPayload = Components.Schemas.JournalSetCreatePayload(
datedOn: datedOn,
description: description,
tag: tag,
journalEntries: entry
)

let input = Operations.CreateJournalSet.Input(
body: .json(.init(journalSet: journalSetPayload))
)

return try await client.createJournalSet(input)
.created.body.json
}

func success(for response: Components.Schemas.JournalSetResponse) -> String {
"Created journal set \(response.journalSet.url)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct JournalSetDeleteCommand: DestructiveCommand {
static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Delete a journal set"
)

@Argument(help: "Journal set ID or URL")
var id: ResourceID

@Flag(help: "Print the request instead of sending it")
var dryRun = false

@Flag(help: "Skip the confirmation prompt")
var yes = false

var confirmation: String {
"Delete journal set \(id)?"
}

func perform(client: Client) async throws -> EmptyResponse {
let input = Operations.DeleteJournalSet.Input(
path: .init(id: id.value)
)

_ = try await client.deleteJournalSet(input).ok

return EmptyResponse()
}

func success(for _: EmptyResponse) -> String {
"Deleted journal set \(id)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct JournalSetListCommand: AsyncPaginatedListCommand {
static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List journal sets",
discussion: "Sets are listed by date, with the undated opening balances set first."
)

static let noun = "journal sets"

static var columns: [Field<Components.Schemas.JournalSet>] {
Field("ID") { .id(url: $0.url) }
Field("Dated On") { .date($0.datedOn) }
Field("Description") { .text($0.description) }
Field("Tag") { .text($0.tag) }
Field("Entries") { .number($0.journalEntries?.count) }
}

@Option(name: .long, help: "Show journal sets dated on or after this date (YYYY-MM-DD)")
var fromDate: String?

@Option(name: .long, help: "Show journal sets dated on or before this date (YYYY-MM-DD)")
var toDate: String?

@Option(name: .long, help: "Show journal sets updated since this date or timestamp")
var updatedSince: String?

@Option(name: .long, help: "Filter by tag, ignoring case")
var tag: String?

@OptionGroup var pagination: PaginationOptions

@Flag(name: .long, help: "Output JSON")
var json = false

func fetch(
client: Client,
page: Int
) async throws -> (response: Components.Schemas.JournalSetListResponse, totalCount: Int?) {
let ok = try await client.listJournalSets(
.init(query: .init(
fromDate: fromDate,
toDate: toDate,
updatedSince: updatedSince,
tag: tag,
page: page,
perPage: pagination.size
))
).ok

return (try ok.body.json, ok.headers.xTotalCount)
}

func items(in response: Components.Schemas.JournalSetListResponse) -> [Components.Schemas.JournalSet] {
response.journalSets
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct JournalSetOpeningBalancesCommand: ShowCommand {
static let configuration = CommandConfiguration(
commandName: "opening-balances",
abstract: "Show the opening balances journal set",
discussion: "Its entries change through journal-set update with its ID; its date and description are locked."
)

static let title = "Opening Balances"

static let sections = JournalSetShowCommand.sections

static let tables = JournalSetShowCommand.tables

@Flag(name: .long, help: "Output JSON")
var json = false

func fetch(client: Client) async throws -> Components.Schemas.JournalSetResponse {
try await client.showOpeningBalances().ok.body.json
}

func record(in response: Components.Schemas.JournalSetResponse) -> Components.Schemas.JournalSet {
response.journalSet
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct JournalSetShowCommand: ShowCommand {
static let configuration = CommandConfiguration(
commandName: "show",
abstract: "Show journal set details"
)

static let title = "Journal Set"

static let tables = [
FieldTable<Components.Schemas.JournalSet>("Entries", items: \.journalEntries) {
Field("ID") { .id(url: $0.url) }
Field("Category") { .id(url: $0.category) }
Field("Description") { .text($0.description) }
Field("User") { .id(url: $0.user) }
Field("Contact") { .id(url: $0.contact) }
Field("Debit Value") { .currency($0.debitValue, code: nil) }
},
FieldTable<Components.Schemas.JournalSet>("Bank Accounts", items: \.bankAccounts) {
openingBalanceColumns
},
FieldTable<Components.Schemas.JournalSet>("Stock Items", items: \.stockItems) {
openingBalanceColumns
},
]

static var sections: [FieldSection<Components.Schemas.JournalSet>] {
FieldSection("Details") {
Field("Description") { .text($0.description) }
Field("Tag") { .text($0.tag) }
}
FieldSection("Dates") {
Field("Dated On") { .date($0.datedOn) }
Field("Updated") { .timestamp($0.updatedAt) }
}
FieldSection("IDs") {
Field("Journal Set") { .id(url: $0.url) }
}
}

@FieldBuilder<Components.Schemas.OpeningBalance>
static var openingBalanceColumns: [Field<Components.Schemas.OpeningBalance>] {
Field("ID") { .id(url: $0.url) }
Field("Description") { .text($0.description) }
Field("Debit Value") { .currency($0.debitValue, code: nil) }
}

@Argument(help: "Journal set ID or URL")
var id: ResourceID

@Flag(name: .long, help: "Output JSON")
var json = false

func fetch(client: Client) async throws -> Components.Schemas.JournalSetResponse {
try await client.showJournalSet(.init(path: .init(id: id.value))).ok.body.json
}

func record(in response: Components.Schemas.JournalSetResponse) -> Components.Schemas.JournalSet {
response.journalSet
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct JournalSetUpdateCommand: MutatingCommand {

// MARK: Internal

static let configuration = CommandConfiguration(
commandName: "update",
abstract: "Update a journal set",
discussion: """
Fields left out are kept. Passing --entry replaces every entry with the ones given, in one request, \
so they must balance on their own.
"""
)

@Argument(help: "Journal set ID or URL")
var id: ResourceID

@Option(name: .long, help: "Date of the entries (YYYY-MM-DD)")
var datedOn: String?

@Option(name: .long, help: "Description")
var description: String?

@Option(name: .long, help: "Tag identifying sets your tool creates - an empty string removes it")
var tag: String?

@Option(name: .long, help: "An entry as a JSON object of the API's entry fields, repeated for each entry")
var entry = [Components.Schemas.JournalEntryPayload]()

@Flag(help: "Print the request instead of sending it")
var dryRun = false

@Flag(name: .long, help: "Output JSON")
var json = false

func perform(client: Client) async throws -> Components.Schemas.JournalSetResponse {
let journalSetPayload = try await Components.Schemas.JournalSetUpdatePayload(
datedOn: datedOn,
description: description,
tag: tag,
journalEntries: journalEntries(client: client)
)

let input = Operations.UpdateJournalSet.Input(
path: .init(id: id.value),
body: .json(.init(journalSet: journalSetPayload))
)

return try await client.updateJournalSet(input)
.ok.body.json
}

func success(for _: Components.Schemas.JournalSetResponse) -> String {
"Updated journal set \(id)"
}

// MARK: Private

private func journalEntries(client: Client) async throws -> [Components.Schemas.JournalEntryPayload]? {
guard !entry.isEmpty else {
return nil
}

let current = try await client.showJournalSet(.init(path: .init(id: id.value)))
.ok.body.json.journalSet.journalEntries ?? []

return current.map { .init(url: ResourceID($0.url).value, _destroy: true) } + entry
}
}
16 changes: 16 additions & 0 deletions Sources/FreeAgentCLI/Commands/JournalSetCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ArgumentParser

struct JournalSetCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "journal-set",
abstract: "Manage journal sets",
subcommands: [
JournalSetListCommand.self,
JournalSetCreateCommand.self,
JournalSetShowCommand.self,
JournalSetUpdateCommand.self,
JournalSetDeleteCommand.self,
JournalSetOpeningBalancesCommand.self,
]
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

extension Components.Schemas.JournalEntryPayload: ExpressibleByArgument {
public init?(argument: String) {
guard let entry = try? JSONDecoder().decode(Self.self, from: Data(argument.utf8)) else {
return nil
}

self = entry
}
}
1 change: 1 addition & 0 deletions Sources/FreeAgentCLI/FreeAgentCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ struct FreeAgentCLI: AsyncParsableCommand {
BankTransactionCommand.self,
ExplanationCommand.self,
CategoryCommand.self,
JournalSetCommand.self,
ExpenseCommand.self,
AttachmentCommand.self,
VatReturnCommand.self,
Expand Down
Loading
Loading