Compare commits

...

53 commits
i18n ... master

Author SHA1 Message Date
James Long 3edf947145 Accidentally only build the sourcemap for API bundle 2022-12-08 15:59:12 -05:00
James Long b34dfb15b2 @actual-app/api 4.1.4 2022-12-08 15:55:29 -05:00
James Long 8b1c5777ad Include the API bundle when publishing 2022-12-08 15:55:05 -05:00
James Long 94c195abb9 Fix API version 2022-12-03 23:34:31 -05:00
James Long 7c1c9bf03a Ignore API bundle 2022-12-03 23:32:57 -05:00
James Long 8f7625831f Prettier 2022-12-03 23:26:11 -05:00
James Long 15e2f2dce7 v22.12.03 2022-12-03 23:26:11 -05:00
Matiss Janis Aboltins 29fb2cc641 Update Account.js 2022-12-03 23:06:59 -05:00
Matiss Janis Aboltins 2566b950c2 feat: ability to add notes to accounts 2022-12-03 23:06:59 -05:00
shall0pass ba71c1ba05 another 2022-12-03 23:03:19 -05:00
shall0pass fcde52a9c7 cleanup 2022-12-03 23:03:19 -05:00
shall0pass 94dbbbc68b cleanup 2022-12-03 23:03:19 -05:00
shall0pass 16e01a8f58 fix 2022-12-03 23:03:19 -05:00
shall0pass a9218e1625 removed additional function 2022-12-03 23:03:19 -05:00
shall0pass 0a61acdf8f Remove the hold for future months button 2022-12-03 23:03:19 -05:00
James Long 157b58a2dd Import only what's needed from the API for importer packages 2022-12-02 10:36:56 -05:00
Matiss Janis Aboltins 7b6909eaa6 fix: add default value to be even more secure against future regressions 2022-11-20 22:49:11 -05:00
Matiss Janis Aboltins 3133ddcda3 fix(useSheetValue): default value should be null not undefined
Fixes #393
2022-11-20 22:49:11 -05:00
James Long 4904da5006 Remove console 2022-11-14 18:01:08 -05:00
James Long a72ee51e1a Always pull in API package from workspace (fixes #378) 2022-11-14 18:01:08 -05:00
Rich Howell bf03dfc1cc
Merge pull request #272 from rickdoesdev/master
a11y: update cleared state display for clarity
2022-11-13 18:34:21 +00:00
James Long a157679906 Fix test 2022-11-12 22:33:49 -05:00
James Long a4a7803407 Fix lint 2022-11-12 22:33:49 -05:00
James Long 2d9b319e45 Move safeNumber to shared util and tweak implementation 2022-11-12 22:33:49 -05:00
Tom French 4b83552ddf feat: add explicit value checking on saving to / reading from budget 2022-11-12 22:33:49 -05:00
Tom French 5f0da9deb8 fix: replace last usages of | 0 2022-11-12 22:33:49 -05:00
Tom French e903f5c20d fix: remove unnecessary conversion to 32 bit 2022-11-12 22:33:49 -05:00
Tom French 04aa1731b5 fix: use Math.round in place of truncating digits 2022-11-12 22:33:49 -05:00
Tom French bb9c9927db fix: use Math.round in place of truncating digits 2022-11-12 22:33:49 -05:00
Tom French 696a094303 fix: replace custom isInteger function with Number.isInteger 2022-11-12 22:33:49 -05:00
Tom French 4421f2a173 fix: use 64bit compatible integer check in aql compiler 2022-11-12 22:33:49 -05:00
Tom French f1b61cf6f1 fix: use integer check which doesn't require value to be 32 bit 2022-11-12 22:33:49 -05:00
Rich Howell 6075e846d3
Merge pull request #266 from j-f1/update-data-file-index
Update data-file-index.txt
2022-11-12 15:36:42 +00:00
Rich Howell 2857e65ccd
Merge pull request #218 from ezfe/master
Fix enter to create accounts
2022-11-12 15:34:57 +00:00
Rich Howell 29124f624b
Merge pull request #389 from shall0pass/help_button
Add a help button to the menu
2022-11-12 15:05:08 +00:00
shall0pass aa97994ad2 fix linting error 2022-11-12 04:53:15 -06:00
shall0pass be3dc26166 launch in separate tab 2022-11-11 18:24:28 -06:00
shall0pass 9ce6f9564c add help button 2022-11-11 18:06:49 -06:00
James Long 12289792da 22.10.25 2022-10-25 10:04:19 -04:00
James Long c8e759fd49 4.1.1 2022-10-25 02:10:57 -04:00
Rich Howell 2256653c16
Merge pull request #373 from rich-howell/remove-docs
Remove documentation
2022-10-23 12:54:13 +01:00
Rich In SQL 182c77a8e3 Remove documentation 2022-10-23 12:48:52 +01:00
Rich Howell de232b3ff0
Merge pull request #368 from rich-howell/readme-updates
Update README.md
2022-10-21 19:11:48 +01:00
Rich In SQL a582975d71 Update README.md
Readme updates
2022-10-21 10:26:47 +01:00
Rich Howell 93f0093c1d
Update stale.yml
Removed auto close
2022-10-20 07:37:53 +01:00
Rich Howell 0e441eeed2
Update stale.yml 2022-10-19 18:48:11 +01:00
Rich Howell f190443272
Create stale.yml 2022-10-19 18:47:08 +01:00
Rick Cuddy 603179dda1 a11y: update cleared state display for clarity
Create new CircleEmpty svg and set uncleared state to use new icon.

Add 'cursor: pointer' to the cleared field to aid in action awareness.
2022-10-12 19:22:24 +11:00
Jed Fox fd3c0f9b18
Update data-file-index.txt 2022-10-07 12:05:23 -04:00
James Long d98b8c1d73 Render a schedule rule with the mapped payee id; fixes crash 2022-10-05 20:14:04 -04:00
Tom French c9a543310d
Remove dollar sign from close account modal (#244) 2022-09-06 11:02:56 +01:00
Tom French 5a00bf6b43
ci: fix CI to an exact node version (#240) 2022-09-02 20:28:33 +01:00
Ezekiel Elin 55b9a0e7ef Fix enter to create accounts 2022-08-29 23:47:07 -04:00
56 changed files with 247 additions and 66776 deletions

View file

@ -6,7 +6,7 @@ runs:
- name: Install node
uses: actions/setup-node@v1
with:
node-version: 16
node-version: 16.15.0
- name: Cache
uses: actions/cache@v2
id: cache

22
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,22 @@
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,72 +1,39 @@
This is the source code for [Actual](https://actualbudget.com), a local-first personal finance tool. It is 100% free and open-source.
## Getting Started
If you are only interested in running the latest version, you don't need this repo. You can get the latest version through npm.
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.
More docs are available in the [docs](https://github.com/actualbudget/actual/tree/master/docs) folder.
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.
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)
Want to say thanks? Click the ⭐ at the top of the page.
Join the [discord](https://discord.gg/pRYNYr4W5A)!
## Key Links
* Actual [discord](https://discord.gg/pRYNYr4W5A) community.
* Actual [Community Documentation](https://actualbudget.github.io/docs)
## 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.
```
git clone https://github.com/actualbudget/actual-server.git
cd actual-server
yarn install
yarn start
```
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)
Navigate to https://localhost:5006 in your browser and you will see Actual.
## Documentation
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
```
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.
## Code structure
The app is split up into a few packages:
The Actual 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 docs are available in the [docs](https://github.com/actualbudget/actual/tree/master/docs) folder.
More information on the project structure is available in our [community documentation](https://actualbudget.github.io/docs/Developers/project-layout).

View file

@ -1,20 +0,0 @@
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

@ -1,18 +0,0 @@
# 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`

View file

@ -1,44 +0,0 @@
# 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
```

1
packages/api/.gitignore vendored Normal file
View file

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

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,8 +1,7 @@
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,9 +1,18 @@
{
"name": "@actual-app/api",
"version": "4.0.2",
"version": "4.1.5",
"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) | 0;
return Math.round(n * 100);
}
function integerToAmount(n) {

View file

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

View file

@ -1,4 +1,5 @@
default-db.sqlite
migrations/.force-copy-windows
migrations/1548957970627_remove-db-version.sql
migrations/1550601598648_payees.sql
migrations/1555786194328_remove_category_group_unique.sql
@ -14,4 +15,3 @@ 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,6 +35,7 @@ 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';
@ -106,6 +107,7 @@ 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;
@ -669,30 +671,46 @@ const AccountHeader = React.memo(
/>
</InitialFocus>
) : isNameEditable ? (
<Button
bare
<View
style={{
fontSize: 25,
fontWeight: 500,
marginLeft: -5,
marginTop: -5,
backgroundColor: 'transparent',
'& svg': { display: 'none' },
'&:hover svg': { display: 'unset' }
flexDirection: 'row',
alignItems: 'center',
gap: 3,
'& .hover-visible': {
opacity: 0,
transition: 'opacity .25s'
},
'&:hover .hover-visible': {
opacity: 1
}
}}
onClick={() => onExposeName(true)}
>
{accountName}
<Pencil1
<View
style={{
width: 11,
height: 11,
marginLeft: 5,
color: colors.n4
fontSize: 25,
fontWeight: 500,
marginRight: 5,
marginBottom: 5
}}
/>
</Button>
>
{accountName}
</View>
<NotesButton id={`account-${account.id}`} />
<Button
bare
className="hover-visible"
onClick={() => onExposeName(true)}
>
<Pencil1
style={{
width: 11,
height: 11,
color: colors.n8
}}
/>
</Button>
</View>
) : (
<View
style={{ fontSize: 25, fontWeight: 500, marginBottom: 5 }}

View file

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

View file

@ -238,9 +238,9 @@ function ScheduleValue({ value }) {
field="rule"
data={schedules}
describe={s => {
let { payee } = extractScheduleConds(s._conditions);
return payee
? `${byId[payee.value].name} (${s.next_date})`
let payeeId = s._payee;
return payeeId
? `${byId[payeeId].name} (${s.next_date})`
: `Next: ${s.next_date}`;
}}
/>

View file

@ -6,6 +6,7 @@ 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';
@ -49,10 +50,15 @@ 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 = CheckCircle1;
Icon = CheckCircleHollow;
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, (width * (427 / 623)) | 0);
// clientWin.setSize(width, (width * (495 / 700)) | 0);
clientWin.setSize(width, Math.floor(width * (427 / 623)));
// clientWin.setSize(width, Math.floor(width * (495 / 700)));
}
});

View file

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

View file

@ -1,9 +1,13 @@
// 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": "^1.0.0",
"@actual-app/api": "*",
"adm-zip": "^0.5.9",
"date-fns": "2.0.0-alpha.27",
"slash": "3.0.0",

View file

@ -1,6 +1,10 @@
// 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": "^1.0.0",
"@actual-app/api": "*",
"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.random() * 30) | 0),
amount: (Math.random() * 10000) | 0,
date: '2020-01-' + pad(Math.floor(Math.random() * 30)),
amount: Math.floor(Math.random() * 10000),
account: accounts[0].id,
notes: 'foo'
};
db.insertTransaction(parent);
db.insertTransaction(
makeChild(parent, {
amount: (Math.random() * 1000) | 0
amount: Math.floor(Math.random() * 1000)
})
);
db.insertTransaction(
makeChild(parent, {
amount: (Math.random() * 1000) | 0
amount: Math.floor(Math.random() * 1000)
})
);
db.insertTransaction(
makeChild(parent, {
amount: (Math.random() * 1000) | 0
amount: Math.floor(Math.random() * 1000)
})
);
} else {
db.insertTransaction({
date: '2020-01-' + pad((Math.random() * 30) | 0),
amount: (Math.random() * 10000) | 0,
date: '2020-01-' + pad(Math.floor(Math.random() * 30)),
amount: Math.floor(Math.random() * 10000),
account: accounts[0].id
});
}

View file

@ -24,21 +24,12 @@ 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,10 +10,6 @@ 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') {
@ -82,7 +78,7 @@ export function makeTransactionSearchQuery(currentQuery, search, dateFormat) {
amount: { $transform: '$abs', $eq: amountToInteger(amount) }
},
amount != null &&
isInteger(amount) && {
Number.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', (i / 5) | 0) });
data.push({ id: i, date: subDays('2020-05-01', Math.floor(i / 5)) });
}
initServer({

View file

@ -11,7 +11,7 @@ import * as monthUtils from '../shared/months';
import q from '../shared/query';
function pickRandom(list) {
return list[((Math.random() * list.length) | 0) % list.length];
return list[Math.floor(Math.random() * list.length) % list.length];
}
function number(start, end) {
@ -19,7 +19,7 @@ function number(start, end) {
}
function integer(start, end) {
return number(start, end) | 0;
return Math.round(number(start, end));
}
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(), (i / 3) | 0),
date: monthUtils.subDays(monthUtils.currentDay(), Math.floor(i / 3)),
category: category.id
};
transactions.push(transaction);
if (Math.random() < 0.2) {
let a = (transaction.amount / 3) | 0;
let a = Math.round(transaction.amount / 3);
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) | 0),
date: monthUtils.subDays(monthUtils.currentDay(), i * 2),
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) | 0),
date: monthUtils.subDays(monthUtils.currentDay(), i * 5),
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.random() * 100000) | 0 : null,
bank: isConnected ? (Math.random() * 10000) | 0 : null,
bankId: isConnected ? (Math.random() * 10000) | 0 : null,
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,
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.random() * 10000 - 7000) | 0,
amount: data.amount || Math.floor(Math.random() * 10000 - 7000),
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, (value | 0) === value ? 'integer' : 'float', {
return typed(value, Number.isInteger(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' && (value | 0) === value) {
if (typeof value === 'number' && Number.isInteger(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((numTransactions / 3) | 0, 3);
let pageCount = Math.max(Math.floor(numTransactions / 3), 3);
let pagedData = [];
let done = false;

View file

@ -1,4 +1,5 @@
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';
@ -6,7 +7,7 @@ import { batchMessages } from '../sync';
async function getSheetValue(sheetName, cell) {
const node = await sheet.getCell(sheetName, cell);
return typeof node.value === 'number' ? node.value : 0;
return safeNumber(typeof node.value === 'number' ? node.value : 0);
}
// We want to only allow the positive movement of money back and
@ -71,9 +72,7 @@ export function getBudget({ category, month }) {
}
export function setBudget({ category, month, amount }) {
if (typeof amount !== 'number') {
amount = 0;
}
amount = safeNumber(typeof amount === 'number' ? amount : 0);
const table = getBudgetTable();
let existing = db.firstSync(
@ -185,32 +184,12 @@ export async function set3MonthAvg({ month }) {
'sum-amount-' + cat.id
);
const avg = ((spent1 + spent2 + spent3) / 3) | 0;
const avg = Math.round((spent1 + spent2 + spent3) / 3);
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 = ?',
@ -233,18 +212,6 @@ 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,15 +12,10 @@ 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,3 +1,4 @@
import { safeNumber } from '../../shared/util';
import * as sheet from '../sheet';
import { number, sumAmounts } from './util';
@ -25,17 +26,17 @@ export async function createCategory(cat, sheetName, prevSheetName) {
],
run: (budgeted, sumAmount, prevCarryover, prevLeftover) => {
if (cat.is_income) {
return (
return safeNumber(
number(budgeted) -
number(sumAmount) +
(prevCarryover ? number(prevLeftover) : 0)
number(sumAmount) +
(prevCarryover ? number(prevLeftover) : 0)
);
}
return (
return safeNumber(
number(budgeted) +
number(sumAmount) +
(prevCarryover ? number(prevLeftover) : 0)
number(sumAmount) +
(prevCarryover ? number(prevLeftover) : 0)
);
}
});
@ -50,7 +51,7 @@ export async function createCategory(cat, sheetName, prevSheetName) {
refresh: true,
run: (budgeted, sumAmount, carryover) => {
return carryover
? Math.max(0, number(budgeted) + number(sumAmount))
? Math.max(0, safeNumber(number(budgeted) + number(sumAmount)))
: sumAmount;
}
});
@ -109,7 +110,7 @@ export function createSummary(groups, categories, sheetName) {
initialValue: 0,
dependencies: ['total-income', 'total-spent'],
run: (income, spent) => {
return income - -spent;
return safeNumber(income - -spent);
}
});
}

View file

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

View file

@ -1,11 +1,14 @@
import { safeNumber } from '../../shared/util';
import { number } from '../spreadsheet/globals';
export { number } from '../spreadsheet/globals';
export function sumAmounts(...amounts) {
return amounts.reduce((total, amount) => {
return total + number(amount);
}, 0);
return safeNumber(
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((timestamp.millis() / 1000 / 60) | 0).toString(3);
let key = Number(Math.floor(timestamp.millis() / 1000 / 60)).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 = (items[next].sort_order | 0) + SORT_INCREMENT;
let order = Math.floor(items[next].sort_order) + 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.random() * shuffled.length) | 0;
let idx = Math.floor(Math.random() * shuffled.length);
if (shuffled[idx]) {
src.push(item);
} else {

View file

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

View file

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

View file

@ -298,6 +298,30 @@ 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);
}
@ -307,8 +331,7 @@ export function toRelaxedInteger(value) {
}
export function integerToCurrency(n) {
// Awesome
return numberFormat.formatter.format(n / 100);
return numberFormat.formatter.format(safeNumber(n) / 100);
}
export function amountToCurrency(n) {
@ -340,7 +363,7 @@ export function amountToInteger(n) {
}
export function integerToAmount(n) {
return parseFloat((n / 100).toFixed(2));
return parseFloat((safeNumber(n) / 100).toFixed(2));
}
// This is used when the input format could be anything (from

View file

@ -368,10 +368,6 @@ 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,10 +204,6 @@ 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"
@ -224,14 +220,6 @@ 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,10 +938,6 @@ 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

@ -116,7 +116,7 @@ function CloseAccount({
<View>
<P>
This account has a balance of{' '}
<strong>${integerToCurrency(balance)}</strong>. To close
<strong>{integerToCurrency(balance)}</strong>. To close
this account, select a different account to transfer this
balance to:
</P>

View file

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

View file

@ -397,6 +397,9 @@ 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;
@ -411,6 +414,7 @@ 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,
value: binding.value === undefined ? null : 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 = (idx / 11) | 0;
const group = Math.floor(idx / 11);
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.random() * 1000000) | 0));
return Promise.resolve('testing-uuid-' + Math.floor(Math.random() * 1000000));
};
uuid.v4Sync = function() {
return 'testing-uuid-' + ((Math.random() * 1000000) | 0);
return 'testing-uuid-' + Math.floor(Math.random() * 1000000);
};
global.__resetWorld = () => {

View file

@ -0,0 +1,19 @@
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,11 +330,6 @@ 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,21 +22,11 @@ __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": ^1.0.0
"@actual-app/api": "*"
adm-zip: ^0.5.9
date-fns: 2.0.0-alpha.27
slash: 3.0.0
@ -50,7 +40,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@actual-app/import-ynab5@workspace:packages/import-ynab5"
dependencies:
"@actual-app/api": ^1.0.0
"@actual-app/api": "*"
date-fns: 2.0.0-alpha.27
uuid: 3.3.2
bin:
@ -8844,7 +8834,7 @@ __metadata:
languageName: node
linkType: hard
"easy-stack@npm:^1.0.0, easy-stack@npm:^1.0.1":
"easy-stack@npm:^1.0.1":
version: 1.0.1
resolution: "easy-stack@npm:1.0.1"
checksum: 161a99e497b3857b0be4ec9e1ebbe90b241ea9d84702f9881b8e5b3f6822065b8c4e33436996935103e191bffba3607de70712a792f4d406a050def48c6bc381
@ -13813,13 +13803,6 @@ 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"
@ -13827,15 +13810,6 @@ 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"
@ -16222,17 +16196,6 @@ 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"