Compare commits

..

1 commit

Author SHA1 Message Date
James Long 2ee0520f6d Render a schedule rule with the mapped payee id; fixes crash 2022-10-04 10:23:02 -04:00
53 changed files with 66772 additions and 243 deletions

View file

@ -1,22 +0,0 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: 90
days-before-issue-close: -1
stale-issue-label: "stale"
stale-issue-message: "🚧🚨 This issue is being marked as stale due to 90 days of inactivity. 🚧🚨"
days-before-pr-stale: -1
days-before-pr-close: -1
only-labels: 'needs triage'
repo-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,39 +1,72 @@
## Getting Started
This is the source code for [Actual](https://actualbudget.com), a local-first personal finance tool. It is 100% free and open-source.
Actual is a local-first personal finance tool. It is 100% free and open-source, written in NodeJS, it has a synchronization element so that all your changes can move between devices without any heavy lifting.
If you are only interested in running the latest version, you don't need this repo. You can get the latest version through npm.
If you are interested in contributing, or want to know how development works, see [CONTRIBUTING.md](https://github.com/actualbudget/actual/blob/master/CONTRIBUTING.md) we would love to have you.
More docs are available in the [docs](https://github.com/actualbudget/actual/tree/master/docs) folder.
Want to say thanks? Click the ⭐ at the top of the page.
If you are interested in contributing, or want to know how development works, see [CONTRIBUTING.md](https://github.com/actualbudget/actual/blob/master/CONTRIBUTING.md)
## Key Links
* Actual [discord](https://discord.gg/pRYNYr4W5A) community.
* Actual [Community Documentation](https://actualbudget.github.io/docs)
Join the [discord](https://discord.gg/pRYNYr4W5A)!
## Installation
If you are only interested in running the latest version and not contributing to the source code, you don't need to clone this repo. You can get the latest version through npm.
**Please Note:** While the Actual repository holds source code for the mobile applications that were supported when Actual was closed source, these are no longer supported on the Open Source version of Actual.
### The easy way: using a server (recommended)
The easiest way to get Actual running is to use the [actual-server](https://github.com/actualbudget/actual-server) project. That is the server for syncing changes across devices, and it comes with the latest version of Actual. The server will provide both the web project and a server for syncing.
You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.github.io/docs/Installing/Local/your-own-machine)
```
git clone https://github.com/actualbudget/actual-server.git
cd actual-server
yarn install
yarn start
```
## Documentation
Navigate to https://localhost:5006 in your browser and you will see Actual.
We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.github.io/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
You should deploy the server somewhere so you can access your data from anywhere. See instructions on the [actual-server](https://github.com/actualbudget/actual-server) repo.
### Without a server
This will give you a fully local web app without a server. This npm package is the `packages/desktop-client` package in this repo built for production:
```
yarn add @actual-app/web
```
Now you need to serve the files in `node_modules/@actual-app/web/build`. One way to do it:
```
cd node_modules/@actual-app/web/build
npx http-server .
```
Navigate to http://localhost:8080 and you should see Actual.
## Building
If you want to build the latest version, see [releasing.md](https://github.com/actualbudget/actual/blob/master/docs/releasing.md). It provides instructions for building this code into the same artifacts that come from npm.
## Run locally
Both the electron and web app can started with a single command. When running in development, it will store data in a `data` directory in the root of the `actual` directory.
First, make sure to run `yarn install` to install all dependencies.
In the root of the project:
```
yarn start # Run the electron app
yarn start:browser # Run the web app
```
## Code structure
The Actual app is split up into a few packages:
The app is split up into a few packages:
* loot-core - The core application that runs on any platform
* loot-design - The generic design components that make up the UI
* desktop-client - The desktop UI
* desktop-electron - The desktop app
* mobile - The mobile app
More information on the project structure is available in our [community documentation](https://actualbudget.github.io/docs/Developers/project-layout).
More docs are available in the [docs](https://github.com/actualbudget/actual/tree/master/docs) folder.

20
docs/API.md Normal file
View file

@ -0,0 +1,20 @@
Previous docs for the API are [here](https://actualbudget.com/docs/developers/using-the-API/). The API is currently being improved. Previously, the API connected to an existing running instance of Actual. Now the API is bundled and fully isolated, capable of running all of Actual itself. Setting up the API is different because of this.
You need to call `init` and pass it the directory where your files live. Call `load-budget` to load the file you want to work on. After that, you can use the same API as before.
Example:
```js
let actual = require('@actual-app/api');
await actual.init({
config: {
dataDir: join(__dirname, 'user-files')
}
});
await actual.internal.send('load-budget', { id: 'My-Finances' });
await actual.getAccounts();
```

View file

@ -0,0 +1,18 @@
# How to build browser for Windows
Many of the build scripts are bash scripts and not natively invokable in Windows. To solve this, you can build the project using Git Bash.
1. Install [Git & Git Bash for Windows](https://git-scm.com/downloads)
2. Install Node v16.x (latest version 17.x does not work due to issue with crypto package)
3. Clone this repo
4. From the root of this repo, run `sh` to launch a bash shell
5. From inside the bash shell, run `yarn install`
6. From still inside the shell, run `yarn start:browser`
7. Open your browser to `localhost:3001`
# How to build electron for Windows
1. Follow steps 1 - 5 above.
2. Run `yarn start`
3. If you get an error from electron, run `yarn rebuild-electron` and rerun `yarn start`;
## rsync: command not found
If you run into this error, you will need to install the rsync binary to Git Bash. Follow the [directions here](https://prasaz.medium.com/add-rsync-to-windows-git-bash-f42736bae1b3). When you get to the final step - installing the libxxhash dll - rename the dll from `msys-xxhash-0.8.0.dll` to `msys-xxhash-0.dll`

44
docs/releasing.md Normal file
View file

@ -0,0 +1,44 @@
# How to cut a release
In the open-source version of Actual, all updates go through npm. There are two libraries:
* `@actual-app/api`: The API for the underlying functionality. This includes the entire backend of Actual, meant to be used with node.
* `@actual-app/web`: A web build that will serve the app with a web frontend. This includes both the frontend and backend of Actual. It includes the backend as well because it's built to be used as a Web Worker.
Both the API and web libraries are versioned together. This may change in the future, but because the web library also brings along its own backend it's easier to maintain a single version for now. That makes it clear which version the backend is regardless of library.
## Releasing `@actual-app/api`
This generates a bundle for the API:
```
cd packages/loot-core
yarn build:api
```
The built files live in `lib-dist`, so we need to copy them to the API package:
```
cp lib-dist/bundle.api* ../api/app
```
Next, bump the version on package.json. Finally, publish it:
```
npm publish
```
## Releasing `@actual-app/web`
In the root of `actual` (not just `desktop-client`), run this:
```
./bin/package-browser
```
This will compile both the backend and the frontend into a single directory in `packages/desktop-client/build`. This directory is all the files that need to be published. After bumping the version, publish `desktop-client`:
```
cd packages/desktop-client
npm publish
```

View file

@ -1 +0,0 @@
app/bundle.api.js*

66420
packages/api/app/bundle.api.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,8 @@
let bundle = require('./app/bundle.api.js');
let injected = require('./injected');
let methods = require('./methods');
let utils = require('./utils');
let injected = require('./injected');
let actualApp;
async function init({ budgetId, config } = {}) {

View file

@ -1,18 +1,9 @@
{
"name": "@actual-app/api",
"version": "4.1.5",
"version": "4.0.2",
"license": "MIT",
"description": "An API for Actual",
"main": "index.js",
"files": [
"app",
"default-db.sqlite",
"index.js",
"injected.js",
"methods.js",
"migrations",
"utils.js"
],
"dependencies": {
"better-sqlite3": "^7.5.0",
"node-fetch": "^1.6.3",

View file

@ -1,5 +1,5 @@
function amountToInteger(n) {
return Math.round(n * 100);
return Math.round(n * 100) | 0;
}
function integerToAmount(n) {

View file

@ -1,6 +1,6 @@
{
"name": "@actual-app/web",
"version": "22.12.03",
"version": "4.0.2",
"license": "MIT",
"files": [
"build"

View file

@ -1,5 +1,4 @@
default-db.sqlite
migrations/.force-copy-windows
migrations/1548957970627_remove-db-version.sql
migrations/1550601598648_payees.sql
migrations/1555786194328_remove_category_group_unique.sql
@ -15,3 +14,4 @@ migrations/1615745967948_meta.sql
migrations/1616167010796_accounts_order.sql
migrations/1618975177358_schedules.sql
migrations/1632571489012_remove_cache.js
migrations/.force-copy-windows

View file

@ -35,7 +35,6 @@ import {
Stack
} from 'loot-design/src/components/common';
import { KeyHandlers } from 'loot-design/src/components/KeyHandlers';
import NotesButton from 'loot-design/src/components/NotesButton';
import CellValue from 'loot-design/src/components/spreadsheet/CellValue';
import format from 'loot-design/src/components/spreadsheet/format';
import useSheetValue from 'loot-design/src/components/spreadsheet/useSheetValue';
@ -107,7 +106,6 @@ function EmptyMessage({ onAdd }) {
function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
let cleared = useSheetValue({
name: balanceQuery.name + '-cleared',
value: 0,
query: balanceQuery.query.filter({ cleared: true })
});
let targetDiff = targetBalance - cleared;
@ -671,46 +669,30 @@ const AccountHeader = React.memo(
/>
</InitialFocus>
) : isNameEditable ? (
<View
<Button
bare
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 3,
'& .hover-visible': {
opacity: 0,
transition: 'opacity .25s'
},
'&:hover .hover-visible': {
opacity: 1
}
fontSize: 25,
fontWeight: 500,
marginLeft: -5,
marginTop: -5,
backgroundColor: 'transparent',
'& svg': { display: 'none' },
'&:hover svg': { display: 'unset' }
}}
onClick={() => onExposeName(true)}
>
<View
style={{
fontSize: 25,
fontWeight: 500,
marginRight: 5,
marginBottom: 5
}}
>
{accountName}
</View>
{accountName}
<NotesButton id={`account-${account.id}`} />
<Button
bare
className="hover-visible"
onClick={() => onExposeName(true)}
>
<Pencil1
style={{
width: 11,
height: 11,
color: colors.n8
}}
/>
</Button>
</View>
<Pencil1
style={{
width: 11,
height: 11,
marginLeft: 5,
color: colors.n4
}}
/>
</Button>
) : (
<View
style={{ fontSize: 25, fontWeight: 500, marginBottom: 5 }}

View file

@ -336,7 +336,7 @@ function StatusCell({
? colors.y5
: selected
? colors.b7
: colors.n7
: colors.n6
};
function onSelect() {
@ -362,8 +362,7 @@ function StatusCell({
':focus': {
border: '1px solid ' + props.color,
boxShadow: `0 1px 2px ${props.color}`
},
cursor: isClearedField ? 'pointer' : 'default'
}
},
isChild && { visibility: 'hidden' }

View file

@ -6,7 +6,6 @@ import { colors } from 'loot-design/src/style';
import AlertTriangle from 'loot-design/src/svg/v2/AlertTriangle';
import CalendarIcon from 'loot-design/src/svg/v2/Calendar';
import CheckCircle1 from 'loot-design/src/svg/v2/CheckCircle1';
import CheckCircleHollow from 'loot-design/src/svg/v2/CheckCircleHollow';
import EditSkull1 from 'loot-design/src/svg/v2/EditSkull1';
import FavoriteStar from 'loot-design/src/svg/v2/FavoriteStar';
import ValidationCheck from 'loot-design/src/svg/v2/ValidationCheck';
@ -50,15 +49,10 @@ export function getStatusProps(status) {
backgroundColor = colors.n11;
Icon = CalendarIcon;
break;
case 'cleared':
color = colors.g5;
backgroundColor = colors.n11;
Icon = CheckCircle1;
break;
default:
color = colors.n1;
backgroundColor = colors.n11;
Icon = CheckCircleHollow;
Icon = CheckCircle1;
break;
}

View file

@ -362,8 +362,8 @@ ipcMain.on('screenshot', () => {
let width = 1100;
// This is for the main screenshot inside the frame
clientWin.setSize(width, Math.floor(width * (427 / 623)));
// clientWin.setSize(width, Math.floor(width * (495 / 700)));
clientWin.setSize(width, (width * (427 / 623)) | 0);
// clientWin.setSize(width, (width * (495 / 700)) | 0);
}
});

View file

@ -3,7 +3,7 @@
"productName": "Actual",
"author": "Shift Reset LLC",
"description": "A simple and powerful personal finance system",
"version": "22.12.03",
"version": "4.0.2",
"scripts": {
"clean": "rm -rf dist",
"build": "electron-builder",

View file

@ -1,13 +1,9 @@
// This is a special usage of the API because this package is embedded
// into Actual itself. We only want to pull in the methods in that
// case and ignore everything else; otherwise we'd be pulling in the
// entire backend bundle from the API
const actual = require('@actual-app/api/methods');
const { amountToInteger } = require('@actual-app/api/utils');
const AdmZip = require('adm-zip');
const d = require('date-fns');
const normalizePathSep = require('slash');
const uuid = require('uuid');
const AdmZip = require('adm-zip');
const actual = require('@actual-app/api');
const amountToInteger = actual.utils.amountToInteger;
// Utils

View file

@ -16,7 +16,7 @@
"bin": "./index.js",
"homepage": "https://github.com/actualbudget/actual/tree/master/packages/import-ynab4#readme",
"dependencies": {
"@actual-app/api": "*",
"@actual-app/api": "^1.0.0",
"adm-zip": "^0.5.9",
"date-fns": "2.0.0-alpha.27",
"slash": "3.0.0",

View file

@ -1,10 +1,6 @@
// This is a special usage of the API because this package is embedded
// into Actual itself. We only want to pull in the methods in that
// case and ignore everything else; otherwise we'd be pulling in the
// entire backend bundle from the API
const actual = require('@actual-app/api/methods');
const d = require('date-fns');
const uuid = require('uuid');
const actual = require('@actual-app/api');
function amountFromYnab(amount) {
// ynabs multiplies amount by 1000 and actual by 100

View file

@ -16,7 +16,7 @@
"bin": "./index.js",
"homepage": "https://github.com/actualbudget/actual/tree/master/packages/import-ynab5#readme",
"dependencies": {
"@actual-app/api": "*",
"@actual-app/api": "^1.0.0",
"date-fns": "2.0.0-alpha.27",
"uuid": "3.3.2"
}

View file

@ -35,31 +35,31 @@ async function init() {
for (let i = 0; i < 100; i++) {
if (Math.random() < 0.02) {
let parent = {
date: '2020-01-' + pad(Math.floor(Math.random() * 30)),
amount: Math.floor(Math.random() * 10000),
date: '2020-01-' + pad((Math.random() * 30) | 0),
amount: (Math.random() * 10000) | 0,
account: accounts[0].id,
notes: 'foo'
};
db.insertTransaction(parent);
db.insertTransaction(
makeChild(parent, {
amount: Math.floor(Math.random() * 1000)
amount: (Math.random() * 1000) | 0
})
);
db.insertTransaction(
makeChild(parent, {
amount: Math.floor(Math.random() * 1000)
amount: (Math.random() * 1000) | 0
})
);
db.insertTransaction(
makeChild(parent, {
amount: Math.floor(Math.random() * 1000)
amount: (Math.random() * 1000) | 0
})
);
} else {
db.insertTransaction({
date: '2020-01-' + pad(Math.floor(Math.random() * 30)),
amount: Math.floor(Math.random() * 10000),
date: '2020-01-' + pad((Math.random() * 30) | 0),
amount: (Math.random() * 10000) | 0,
account: accounts[0].id
});
}

View file

@ -24,12 +24,21 @@ export function applyBudgetAction(month, type, args) {
case 'set-3-avg':
await send('budget/set-3month-avg', { month });
break;
case 'set-all-future':
await send('budget/set-all-future', { startMonth: month });
break;
case 'hold':
await send('budget/hold-for-next-month', {
month,
amount: args.amount
});
break;
case 'hold-all-future':
await send('budget/hold-for-future-months', {
startMonth: month,
amount: args.amount
});
break;
case 'reset-hold':
await send('budget/reset-hold', { month });
break;

View file

@ -10,6 +10,10 @@ import {
import q from '../shared/query';
import { currencyToAmount, amountToInteger } from '../shared/util';
function isInteger(num) {
return (num | 0) === num;
}
export function getAccountFilter(accountId, field = 'account') {
if (accountId) {
if (accountId === 'budgeted') {
@ -78,7 +82,7 @@ export function makeTransactionSearchQuery(currentQuery, search, dateFormat) {
amount: { $transform: '$abs', $eq: amountToInteger(amount) }
},
amount != null &&
Number.isInteger(amount) && {
isInteger(amount) && {
amount: {
$transform: { $abs: { $idiv: ['$', 100] } },
$eq: amount

View file

@ -93,7 +93,7 @@ function initBasicServer(delay) {
function initPagingServer(dataLength, { delay, eventType = 'select' } = {}) {
let data = [];
for (let i = 0; i < dataLength; i++) {
data.push({ id: i, date: subDays('2020-05-01', Math.floor(i / 5)) });
data.push({ id: i, date: subDays('2020-05-01', (i / 5) | 0) });
}
initServer({

View file

@ -11,7 +11,7 @@ import * as monthUtils from '../shared/months';
import q from '../shared/query';
function pickRandom(list) {
return list[Math.floor(Math.random() * list.length) % list.length];
return list[((Math.random() * list.length) | 0) % list.length];
}
function number(start, end) {
@ -19,7 +19,7 @@ function number(start, end) {
}
function integer(start, end) {
return Math.round(number(start, end));
return number(start, end) | 0;
}
function findMin(items, field) {
@ -104,13 +104,13 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
amount,
payee: payee.id,
account: account.id,
date: monthUtils.subDays(monthUtils.currentDay(), Math.floor(i / 3)),
date: monthUtils.subDays(monthUtils.currentDay(), (i / 3) | 0),
category: category.id
};
transactions.push(transaction);
if (Math.random() < 0.2) {
let a = Math.round(transaction.amount / 3);
let a = (transaction.amount / 3) | 0;
let pick = () =>
payee === incomePayee
? incomeGroup.categories.find(c => c.name === 'Income').id
@ -244,7 +244,7 @@ async function fillChecking(handlers, account, payees, groups) {
amount,
payee: payee.id,
account: account.id,
date: monthUtils.subDays(monthUtils.currentDay(), i * 2),
date: monthUtils.subDays(monthUtils.currentDay(), (i * 2) | 0),
category: category.id
});
}
@ -334,7 +334,7 @@ async function fillSavings(handlers, account, payees, groups) {
amount,
payee: payee.id,
account: account.id,
date: monthUtils.subDays(monthUtils.currentDay(), i * 5),
date: monthUtils.subDays(monthUtils.currentDay(), (i * 5) | 0),
category: category.id
});
}

View file

@ -6,9 +6,9 @@ export function generateAccount(name, isConnected, type, offbudget) {
return {
id: uuid.v4Sync(),
name,
balance_current: isConnected ? Math.floor(Math.random() * 100000) : null,
bank: isConnected ? Math.floor(Math.random() * 10000) : null,
bankId: isConnected ? Math.floor(Math.random() * 10000) : null,
balance_current: isConnected ? (Math.random() * 100000) | 0 : null,
bank: isConnected ? (Math.random() * 10000) | 0 : null,
bankId: isConnected ? (Math.random() * 10000) | 0 : null,
bankName: isConnected ? 'boa' : null,
type: type || 'checking',
offbudget: offbudget ? 1 : 0,
@ -54,7 +54,7 @@ function _generateTransaction(data) {
const id = data.id || uuid.v4Sync();
return {
id: id,
amount: data.amount || Math.floor(Math.random() * 10000 - 7000),
amount: data.amount || (Math.random() * 10000 - 7000) | 0,
payee: data.payee || (Math.random() < 0.9 ? 'payed-to' : 'guy'),
notes:
Math.random() < 0.1 ? 'A really long note that should overflow' : 'Notes',

View file

@ -456,7 +456,7 @@ function compileLiteral(value) {
} else if (typeof value === 'boolean') {
return typed(value ? 1 : 0, 'boolean', { literal: true });
} else if (typeof value === 'number') {
return typed(value, Number.isInteger(value) ? 'integer' : 'float', {
return typed(value, (value | 0) === value ? 'integer' : 'float', {
literal: true
});
} else if (Array.isArray(value)) {

View file

@ -42,7 +42,7 @@ export function convertInputType(value, type) {
}
return value;
case 'integer':
if (typeof value === 'number' && Number.isInteger(value)) {
if (typeof value === 'number' && (value | 0) === value) {
return value;
} else {
throw new Error("Can't convert to integer: " + JSON.stringify(value));

View file

@ -91,7 +91,7 @@ function expectTransactionOrder(data, fields) {
}
async function expectPagedData(query, numTransactions, allData) {
let pageCount = Math.max(Math.floor(numTransactions / 3), 3);
let pageCount = Math.max((numTransactions / 3) | 0, 3);
let pagedData = [];
let done = false;

View file

@ -1,5 +1,4 @@
import * as monthUtils from '../../shared/months';
import { safeNumber } from '../../shared/util';
import * as db from '../db';
import * as prefs from '../prefs';
import * as sheet from '../sheet';
@ -7,7 +6,7 @@ import { batchMessages } from '../sync';
async function getSheetValue(sheetName, cell) {
const node = await sheet.getCell(sheetName, cell);
return safeNumber(typeof node.value === 'number' ? node.value : 0);
return typeof node.value === 'number' ? node.value : 0;
}
// We want to only allow the positive movement of money back and
@ -72,7 +71,9 @@ export function getBudget({ category, month }) {
}
export function setBudget({ category, month, amount }) {
amount = safeNumber(typeof amount === 'number' ? amount : 0);
if (typeof amount !== 'number') {
amount = 0;
}
const table = getBudgetTable();
let existing = db.firstSync(
@ -184,12 +185,32 @@ export async function set3MonthAvg({ month }) {
'sum-amount-' + cat.id
);
const avg = Math.round((spent1 + spent2 + spent3) / 3);
const avg = ((spent1 + spent2 + spent3) / 3) | 0;
setBudget({ category: cat.id, month, amount: -avg });
}
});
}
export async function setAllFuture({ startMonth }) {
if (!isReflectBudget()) {
throw new Error('setAllFuture only applies to report budget type');
}
let table = getBudgetTable();
let budgetData = await getBudgetData(table, dbMonth(startMonth));
let months = getAllMonths(monthUtils.addMonths(startMonth, 1));
batchMessages(() => {
for (let month of months) {
budgetData.forEach(budget => {
if (budget.is_income === 1 && !isReflectBudget()) {
return;
}
setBudget({ category: budget.category, month, amount: budget.amount });
});
}
});
}
export async function holdForNextMonth({ month, amount }) {
let row = await db.first(
'SELECT buffered FROM zero_budget_months WHERE id = ?',
@ -212,6 +233,18 @@ export async function holdForNextMonth({ month, amount }) {
return false;
}
export async function holdForFutureMonths({ startMonth, amount }) {
let months = getAllMonths(startMonth);
await batchMessages(async () => {
for (let month of months) {
if (!(await holdForNextMonth({ month, amount }))) {
break;
}
}
});
}
export async function resetHold({ month }) {
await setBuffer(month, 0);
}

View file

@ -12,10 +12,15 @@ app.method(
);
app.method('budget/set-zero', mutator(undoable(actions.setZero)));
app.method('budget/set-3month-avg', mutator(undoable(actions.set3MonthAvg)));
app.method('budget/set-all-future', mutator(undoable(actions.setAllFuture)));
app.method(
'budget/hold-for-next-month',
mutator(undoable(actions.holdForNextMonth))
);
app.method(
'budget/hold-for-future-months',
mutator(undoable(actions.holdForFutureMonths))
);
app.method('budget/reset-hold', mutator(undoable(actions.resetHold)));
app.method(
'budget/cover-overspending',

View file

@ -1,4 +1,3 @@
import { safeNumber } from '../../shared/util';
import * as sheet from '../sheet';
import { number, sumAmounts } from './util';
@ -26,17 +25,17 @@ export async function createCategory(cat, sheetName, prevSheetName) {
],
run: (budgeted, sumAmount, prevCarryover, prevLeftover) => {
if (cat.is_income) {
return safeNumber(
return (
number(budgeted) -
number(sumAmount) +
(prevCarryover ? number(prevLeftover) : 0)
number(sumAmount) +
(prevCarryover ? number(prevLeftover) : 0)
);
}
return safeNumber(
return (
number(budgeted) +
number(sumAmount) +
(prevCarryover ? number(prevLeftover) : 0)
number(sumAmount) +
(prevCarryover ? number(prevLeftover) : 0)
);
}
});
@ -51,7 +50,7 @@ export async function createCategory(cat, sheetName, prevSheetName) {
refresh: true,
run: (budgeted, sumAmount, carryover) => {
return carryover
? Math.max(0, safeNumber(number(budgeted) + number(sumAmount)))
? Math.max(0, number(budgeted) + number(sumAmount))
: sumAmount;
}
});
@ -110,7 +109,7 @@ export function createSummary(groups, categories, sheetName) {
initialValue: 0,
dependencies: ['total-income', 'total-spent'],
run: (income, spent) => {
return safeNumber(income - -spent);
return income - -spent;
}
});
}

View file

@ -1,5 +1,4 @@
import * as monthUtils from '../../shared/months';
import { safeNumber } from '../../shared/util';
import * as sheet from '../sheet';
import { number, sumAmounts, flatten2, unflatten2 } from './util';
@ -52,10 +51,10 @@ export function createCategory(cat, sheetName, prevSheetName) {
`${prevSheetName}!leftover-pos-${cat.id}`
],
run: (budgeted, spent, prevCarryover, prevLeftover, prevLeftoverPos) => {
return safeNumber(
return (
number(budgeted) +
number(spent) +
(prevCarryover ? number(prevLeftover) : number(prevLeftoverPos))
number(spent) +
(prevCarryover ? number(prevLeftover) : number(prevLeftoverPos))
);
}
});
@ -79,7 +78,7 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
sheet.get().createDynamic(sheetName, 'from-last-month', {
initialValue: 0,
dependencies: [`${prevSheetName}!to-budget`, `${prevSheetName}!buffered`],
run: (toBudget, buffered) => safeNumber(number(toBudget) + number(buffered))
run: (toBudget, buffered) => number(toBudget) + number(buffered)
});
// Alias the group income total to `total-income`
@ -92,8 +91,7 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
sheet.get().createDynamic(sheetName, 'available-funds', {
initialValue: 0,
dependencies: ['total-income', 'from-last-month'],
run: (income, fromLastMonth) =>
safeNumber(number(income) + number(fromLastMonth))
run: (income, fromLastMonth) => number(income) + number(fromLastMonth)
});
sheet.get().createDynamic(sheetName, 'last-month-overspent', {
@ -106,14 +104,12 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
),
run: (...data) => {
data = unflatten2(data);
return safeNumber(
data.reduce((total, [leftover, carryover]) => {
if (carryover) {
return total;
}
return total + Math.min(0, number(leftover));
}, 0)
);
return data.reduce((total, [leftover, carryover]) => {
if (carryover) {
return total;
}
return total + Math.min(0, number(leftover));
}, 0);
}
});
@ -139,11 +135,11 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
'buffered'
],
run: (available, lastOverspent, totalBudgeted, buffered) => {
return safeNumber(
return (
number(available) +
number(lastOverspent) +
number(totalBudgeted) -
number(buffered)
number(lastOverspent) +
number(totalBudgeted) -
number(buffered)
);
}
});

View file

@ -1,14 +1,11 @@
import { safeNumber } from '../../shared/util';
import { number } from '../spreadsheet/globals';
export { number } from '../spreadsheet/globals';
export function sumAmounts(...amounts) {
return safeNumber(
amounts.reduce((total, amount) => {
return total + number(amount);
}, 0)
);
return amounts.reduce((total, amount) => {
return total + number(amount);
}, 0);
}
export function flatten2(arr) {

View file

@ -22,7 +22,7 @@ export function keyToTimestamp(key) {
export function insert(trie, timestamp) {
let hash = timestamp.hash();
let key = Number(Math.floor(timestamp.millis() / 1000 / 60)).toString(3);
let key = Number((timestamp.millis() / 1000 / 60) | 0).toString(3);
trie = Object.assign({}, trie, { hash: trie.hash ^ hash });
return insertKey(trie, key, hash);

View file

@ -34,7 +34,7 @@ export function shoveSortOrders(items, targetId) {
} else {
if (target.sort_order - (before ? before.sort_order : 0) <= 2) {
let next = to;
let order = Math.floor(items[next].sort_order) + SORT_INCREMENT;
let order = (items[next].sort_order | 0) + SORT_INCREMENT;
while (next < items.length) {
// No need to update it if it's already greater than the current
// order. This can happen because there may already be large

View file

@ -171,7 +171,7 @@ function shuffle(arr) {
let shuffled = new Array(src.length);
let item;
while ((item = src.pop())) {
let idx = Math.floor(Math.random() * shuffled.length);
let idx = (Math.random() * shuffled.length) | 0;
if (shuffled[idx]) {
src.push(item);
} else {

View file

@ -199,5 +199,5 @@ export function makeValue(value, cond) {
}
export function getApproxNumberThreshold(number) {
return Math.round(Math.abs(number) * 0.075);
return (Math.abs(number) * 0.075) | 0;
}

View file

@ -221,7 +221,7 @@ export function extractScheduleConds(conditions) {
export function getScheduledAmount(amount) {
if (amount && typeof amount !== 'number') {
return Math.round((amount.num1 + amount.num2) / 2);
return ((amount.num1 + amount.num2) / 2) | 0;
}
return amount;
}

View file

@ -298,30 +298,6 @@ export function getNumberFormat() {
setNumberFormat('comma-dot');
// Number utilities
// We dont use `Number.MAX_SAFE_NUMBER` and such here because those
// numbers are so large that it's not safe to convert them to floats
// (i.e. N / 100). For example, `9007199254740987 / 100 ===
// 90071992547409.88`. While the internal arithemetic would be correct
// because we always do that on numbers, the app would potentially
// display wrong numbers. Instead of `2**53` we use `2**51` which
// gives division more room to be correct
const MAX_SAFE_NUMBER = 2 ** 51 - 1;
const MIN_SAFE_NUMBER = -MAX_SAFE_NUMBER;
export function safeNumber(value) {
if (!Number.isInteger(value)) {
throw new Error('safeNumber: number is not an integer: ' + value);
}
if (value > MAX_SAFE_NUMBER || value < MIN_SAFE_NUMBER) {
throw new Error(
"safeNumber: can't safely perform arithmetic with number: " + value
);
}
return value;
}
export function toRelaxedNumber(value) {
return integerToAmount(currencyToInteger(value) || 0);
}
@ -331,7 +307,8 @@ export function toRelaxedInteger(value) {
}
export function integerToCurrency(n) {
return numberFormat.formatter.format(safeNumber(n) / 100);
// Awesome
return numberFormat.formatter.format(n / 100);
}
export function amountToCurrency(n) {
@ -363,7 +340,7 @@ export function amountToInteger(n) {
}
export function integerToAmount(n) {
return parseFloat((safeNumber(n) / 100).toFixed(2));
return parseFloat((n / 100).toFixed(2));
}
// This is used when the input format could be anything (from

View file

@ -368,6 +368,10 @@ export default React.memo(function BudgetSummary({ month }) {
{
name: 'set-3-avg',
text: 'Set budgets to 3 month avg'
},
{
name: 'set-all-future',
text: 'Apply to all future budgets'
}
]}
/>

View file

@ -204,6 +204,10 @@ function ToBudget({ month, prevMonthName, collapsed, onBudgetAction }) {
name: 'buffer',
text: 'Hold for next month'
},
{
name: 'buffer-future',
text: 'Hold for all future months'
},
{
name: 'reset-buffer',
text: "Reset next month's buffer"
@ -220,6 +224,14 @@ function ToBudget({ month, prevMonthName, collapsed, onBudgetAction }) {
}}
/>
)}
{state.menuOpen === 'buffer-future' && (
<HoldTooltip
onClose={() => setState({ menuOpen: null })}
onSubmit={amount => {
onBudgetAction(month, 'hold-all-future', { amount });
}}
/>
)}
{state.menuOpen === 'transfer' && (
<TransferTooltip
initialAmountName="leftover"

View file

@ -938,6 +938,10 @@ export function ModalButtons({
style
]}
>
{/* Add a dummy button first so that when a user
presses "enter" they do a normal submit, instead of
activating the back button */}
<Button data-hidden={true} style={{ display: 'none' }} />
{leftContent}
<View style={{ flex: 1 }} />
{children}

View file

@ -146,9 +146,7 @@ function CreateLocalAccount({ modalProps, actions, history }) {
)}
<ModalButtons>
<Button onClick={() => modalProps.onBack()} type="button">
Back
</Button>
<Button onClick={() => modalProps.onBack()}>Back</Button>
<Button primary style={{ marginLeft: 10 }}>
Create
</Button>

View file

@ -397,9 +397,6 @@ const MenuButton = withRouter(function MenuButton({ history }) {
case 'settings':
history.push('/settings');
break;
case 'help':
window.open('https://actualbudget.github.io/docs', '_blank');
break;
case 'close':
dispatch(closeBudget());
break;
@ -414,7 +411,6 @@ const MenuButton = withRouter(function MenuButton({ history }) {
{ name: 'repair-splits', text: 'Repair split transactions' },
Menu.line,
{ name: 'settings', text: 'Settings' },
{ name: 'help', text: 'Help' },
{ name: 'close', text: 'Close File' }
];

View file

@ -41,7 +41,7 @@ export default function useSheetValue(binding, onChange) {
let spreadsheet = useContext(SpreadsheetContext);
let [result, setResult] = useState({
name: sheetName + '!' + binding.name,
value: binding.value === undefined ? null : binding.value,
value: binding.value,
query: binding.query
});
let latestOnChange = useRef(onChange);

View file

@ -7,7 +7,7 @@ let groups = ['y', 'r', 'b', 'n', 'g', 'p'];
let colors = {};
list.forEach((color, idx) => {
const group = Math.floor(idx / 11);
const group = (idx / 11) | 0;
const n = idx % 11;
colors[groups[group] + (n + 1)] = color;

View file

@ -24,11 +24,11 @@ global.Date.now = () => 123456789;
let seqId = 1;
uuid.v4 = function() {
return Promise.resolve('testing-uuid-' + Math.floor(Math.random() * 1000000));
return Promise.resolve('testing-uuid-' + ((Math.random() * 1000000) | 0));
};
uuid.v4Sync = function() {
return 'testing-uuid-' + Math.floor(Math.random() * 1000000);
return 'testing-uuid-' + ((Math.random() * 1000000) | 0);
};
global.__resetWorld = () => {

View file

@ -1,19 +0,0 @@
import React from 'react';
const SvgCheckCircleHollow = props => (
<svg
{...props}
viewBox="0 0 24 24"
style={{
color: '#242134',
...props.style
}}
>
<path
d="M 12 0 C 1.3084197 0 -4.0435475 12.925204 3.515625 20.484375 C 11.074797 28.043547 24 22.69158 24 12 C 23.992285 5.3757944 18.624205 0.0077147446 12 0 z M 12.009766 1.9882812 C 17.531104 1.9947115 22.005288 6.4688953 22.011719 11.990234 C 22.011719 20.90177 11.238144 25.363144 4.9375 19.0625 C -1.3631434 12.761856 3.0982293 1.9882812 12.009766 1.9882812 z M 18.244141 6.5761719 A 1 1 0 0 0 17.316406 7.0175781 L 11.089844 15.46875 L 7.0136719 12.207031 A 1.0004882 1.0004882 0 1 0 5.7636719 13.769531 L 10.652344 17.677734 A 1.011 1.011 0 0 0 12.082031 17.488281 L 18.927734 8.1992188 A 1 1 0 0 0 18.244141 6.5761719 z "
fill="currentColor"
/>
</svg>
);
export default SvgCheckCircleHollow;

View file

@ -330,6 +330,11 @@ class Budget extends React.Component {
case 3:
this.onBudgetAction('set-3-avg');
break;
case 4:
if (budgetType === 'report') {
this.onBudgetAction('set-all-future');
break;
}
default:
}
}

View file

@ -22,11 +22,21 @@ __metadata:
languageName: unknown
linkType: soft
"@actual-app/api@npm:^1.0.0":
version: 1.1.3
resolution: "@actual-app/api@npm:1.1.3"
dependencies:
node-ipc: 9.1.1
uuid: 3.3.2
checksum: e7fccff7583d64ac908eb7a7c93226200fd75af92b9fe9718b6e3fe0d004d92d79d87485e212b0d3d86cb685827e6733c939ece799156eea64db886bf1457a94
languageName: node
linkType: hard
"@actual-app/import-ynab4@*, @actual-app/import-ynab4@workspace:packages/import-ynab4":
version: 0.0.0-use.local
resolution: "@actual-app/import-ynab4@workspace:packages/import-ynab4"
dependencies:
"@actual-app/api": "*"
"@actual-app/api": ^1.0.0
adm-zip: ^0.5.9
date-fns: 2.0.0-alpha.27
slash: 3.0.0
@ -40,7 +50,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@actual-app/import-ynab5@workspace:packages/import-ynab5"
dependencies:
"@actual-app/api": "*"
"@actual-app/api": ^1.0.0
date-fns: 2.0.0-alpha.27
uuid: 3.3.2
bin:
@ -8834,7 +8844,7 @@ __metadata:
languageName: node
linkType: hard
"easy-stack@npm:^1.0.1":
"easy-stack@npm:^1.0.0, easy-stack@npm:^1.0.1":
version: 1.0.1
resolution: "easy-stack@npm:1.0.1"
checksum: 161a99e497b3857b0be4ec9e1ebbe90b241ea9d84702f9881b8e5b3f6822065b8c4e33436996935103e191bffba3607de70712a792f4d406a050def48c6bc381
@ -13803,6 +13813,13 @@ jest-snapshot@test:
languageName: node
linkType: hard
"js-message@npm:1.0.5":
version: 1.0.5
resolution: "js-message@npm:1.0.5"
checksum: fd2fc8837a88a115aa2fa859bf5c13d9b335fd7eeba8426c44da6eb006b04c52cfe6675b3c27d6b112ffc51dadb8bc51d58340c3a3aa5c555d7da6bdc72ce9c0
languageName: node
linkType: hard
"js-message@npm:1.0.7":
version: 1.0.7
resolution: "js-message@npm:1.0.7"
@ -13810,6 +13827,15 @@ jest-snapshot@test:
languageName: node
linkType: hard
"js-queue@npm:2.0.0":
version: 2.0.0
resolution: "js-queue@npm:2.0.0"
dependencies:
easy-stack: ^1.0.0
checksum: 8f8e589cc20fd3bc3067db73ecaac77b55411c3ac58fdd6882868924ee19ab4203d19e68d3ec680c5c8f5e8282e30dafa377014dbec05c3f2d33be4596f4fb65
languageName: node
linkType: hard
"js-queue@npm:2.0.2":
version: 2.0.2
resolution: "js-queue@npm:2.0.2"
@ -16196,6 +16222,17 @@ jest-snapshot@test:
languageName: node
linkType: hard
"node-ipc@npm:9.1.1":
version: 9.1.1
resolution: "node-ipc@npm:9.1.1"
dependencies:
event-pubsub: 4.3.0
js-message: 1.0.5
js-queue: 2.0.0
checksum: 2b66099d1976e4328d34ae7fec853d3969ca337b52b5aefb48ae1d19387c37d6716c2b98d4a4934ec24aa79f0441721961d6c1beb858c294ad6a7a97ddf5460d
languageName: node
linkType: hard
"node-ipc@npm:9.1.4":
version: 9.1.4
resolution: "node-ipc@npm:9.1.4"