Compare commits
41 commits
remove-32b
...
master
Author | SHA1 | Date | |
---|---|---|---|
3edf947145 | |||
b34dfb15b2 | |||
8b1c5777ad | |||
94c195abb9 | |||
7c1c9bf03a | |||
8f7625831f | |||
15e2f2dce7 | |||
29fb2cc641 | |||
2566b950c2 | |||
ba71c1ba05 | |||
fcde52a9c7 | |||
94dbbbc68b | |||
16e01a8f58 | |||
a9218e1625 | |||
0a61acdf8f | |||
157b58a2dd | |||
7b6909eaa6 | |||
3133ddcda3 | |||
4904da5006 | |||
a72ee51e1a | |||
bf03dfc1cc | |||
a157679906 | |||
a4a7803407 | |||
2d9b319e45 | |||
4b83552ddf | |||
5f0da9deb8 | |||
e903f5c20d | |||
04aa1731b5 | |||
bb9c9927db | |||
696a094303 | |||
4421f2a173 | |||
f1b61cf6f1 | |||
6075e846d3 | |||
2857e65ccd | |||
29124f624b | |||
aa97994ad2 | |||
be3dc26166 | |||
9ce6f9564c | |||
603179dda1 | |||
fd3c0f9b18 | |||
55b9a0e7ef |
1
packages/api/.gitignore
vendored
Normal file
1
packages/api/.gitignore
vendored
Normal 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
|
@ -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 } = {}) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function amountToInteger(n) {
|
||||
return Math.round(n * 100) | 0;
|
||||
return Math.round(n * 100);
|
||||
}
|
||||
|
||||
function integerToAmount(n) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "22.10.25",
|
||||
"version": "22.12.03",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "Actual",
|
||||
"author": "Shift Reset LLC",
|
||||
"description": "A simple and powerful personal finance system",
|
||||
"version": "22.10.25",
|
||||
"version": "22.12.03",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"build": "electron-builder",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' }
|
||||
];
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
19
packages/loot-design/src/svg/v2/CheckCircleHollow.js
Normal file
19
packages/loot-design/src/svg/v2/CheckCircleHollow.js
Normal 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;
|
|
@ -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:
|
||||
}
|
||||
}
|
||||
|
|
43
yarn.lock
43
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue