Compare commits

..

2 commits

Author SHA1 Message Date
James Long
dbda0ec4c2 Remove console 2022-11-09 23:43:11 -05:00
James Long
cd0fc02a44 Always pull in API package from workspace (fixes #378) 2022-11-09 23:33:55 -05:00
45 changed files with 66598 additions and 199 deletions

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 bundle = require('./app/bundle.api.js');
let injected = require('./injected');
let methods = require('./methods'); let methods = require('./methods');
let utils = require('./utils'); let utils = require('./utils');
let injected = require('./injected');
let actualApp; let actualApp;
async function init({ budgetId, config } = {}) { async function init({ budgetId, config } = {}) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -336,7 +336,7 @@ function StatusCell({
? colors.y5 ? colors.y5
: selected : selected
? colors.b7 ? colors.b7
: colors.n7 : colors.n6
}; };
function onSelect() { function onSelect() {
@ -362,8 +362,7 @@ function StatusCell({
':focus': { ':focus': {
border: '1px solid ' + props.color, border: '1px solid ' + props.color,
boxShadow: `0 1px 2px ${props.color}` boxShadow: `0 1px 2px ${props.color}`
}, }
cursor: isClearedField ? 'pointer' : 'default'
}, },
isChild && { visibility: 'hidden' } 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 AlertTriangle from 'loot-design/src/svg/v2/AlertTriangle';
import CalendarIcon from 'loot-design/src/svg/v2/Calendar'; import CalendarIcon from 'loot-design/src/svg/v2/Calendar';
import CheckCircle1 from 'loot-design/src/svg/v2/CheckCircle1'; 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 EditSkull1 from 'loot-design/src/svg/v2/EditSkull1';
import FavoriteStar from 'loot-design/src/svg/v2/FavoriteStar'; import FavoriteStar from 'loot-design/src/svg/v2/FavoriteStar';
import ValidationCheck from 'loot-design/src/svg/v2/ValidationCheck'; import ValidationCheck from 'loot-design/src/svg/v2/ValidationCheck';
@ -50,15 +49,10 @@ export function getStatusProps(status) {
backgroundColor = colors.n11; backgroundColor = colors.n11;
Icon = CalendarIcon; Icon = CalendarIcon;
break; break;
case 'cleared':
color = colors.g5;
backgroundColor = colors.n11;
Icon = CheckCircle1;
break;
default: default:
color = colors.n1; color = colors.n1;
backgroundColor = colors.n11; backgroundColor = colors.n11;
Icon = CheckCircleHollow; Icon = CheckCircle1;
break; break;
} }

View file

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

View file

@ -3,7 +3,7 @@
"productName": "Actual", "productName": "Actual",
"author": "Shift Reset LLC", "author": "Shift Reset LLC",
"description": "A simple and powerful personal finance system", "description": "A simple and powerful personal finance system",
"version": "22.12.03", "version": "22.10.25",
"scripts": { "scripts": {
"clean": "rm -rf dist", "clean": "rm -rf dist",
"build": "electron-builder", "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 d = require('date-fns');
const normalizePathSep = require('slash'); const normalizePathSep = require('slash');
const uuid = require('uuid'); const uuid = require('uuid');
const AdmZip = require('adm-zip');
const actual = require('@actual-app/api');
const amountToInteger = actual.utils.amountToInteger;
// Utils // Utils

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 d = require('date-fns');
const uuid = require('uuid'); const uuid = require('uuid');
const actual = require('@actual-app/api');
function amountFromYnab(amount) { function amountFromYnab(amount) {
// ynabs multiplies amount by 1000 and actual by 100 // ynabs multiplies amount by 1000 and actual by 100

View file

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

View file

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

View file

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

View file

@ -93,7 +93,7 @@ function initBasicServer(delay) {
function initPagingServer(dataLength, { delay, eventType = 'select' } = {}) { function initPagingServer(dataLength, { delay, eventType = 'select' } = {}) {
let data = []; let data = [];
for (let i = 0; i < dataLength; i++) { 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({ initServer({

View file

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

View file

@ -6,9 +6,9 @@ export function generateAccount(name, isConnected, type, offbudget) {
return { return {
id: uuid.v4Sync(), id: uuid.v4Sync(),
name, name,
balance_current: isConnected ? Math.floor(Math.random() * 100000) : null, balance_current: isConnected ? (Math.random() * 100000) | 0 : null,
bank: isConnected ? Math.floor(Math.random() * 10000) : null, bank: isConnected ? (Math.random() * 10000) | 0 : null,
bankId: isConnected ? Math.floor(Math.random() * 10000) : null, bankId: isConnected ? (Math.random() * 10000) | 0 : null,
bankName: isConnected ? 'boa' : null, bankName: isConnected ? 'boa' : null,
type: type || 'checking', type: type || 'checking',
offbudget: offbudget ? 1 : 0, offbudget: offbudget ? 1 : 0,
@ -54,7 +54,7 @@ function _generateTransaction(data) {
const id = data.id || uuid.v4Sync(); const id = data.id || uuid.v4Sync();
return { return {
id: id, 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'), payee: data.payee || (Math.random() < 0.9 ? 'payed-to' : 'guy'),
notes: notes:
Math.random() < 0.1 ? 'A really long note that should overflow' : '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') { } else if (typeof value === 'boolean') {
return typed(value ? 1 : 0, 'boolean', { literal: true }); return typed(value ? 1 : 0, 'boolean', { literal: true });
} else if (typeof value === 'number') { } else if (typeof value === 'number') {
return typed(value, Number.isInteger(value) ? 'integer' : 'float', { return typed(value, (value | 0) === value ? 'integer' : 'float', {
literal: true literal: true
}); });
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {

View file

@ -42,7 +42,7 @@ export function convertInputType(value, type) {
} }
return value; return value;
case 'integer': case 'integer':
if (typeof value === 'number' && Number.isInteger(value)) { if (typeof value === 'number' && (value | 0) === value) {
return value; return value;
} else { } else {
throw new Error("Can't convert to integer: " + JSON.stringify(value)); 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) { 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 pagedData = [];
let done = false; let done = false;

View file

@ -1,5 +1,4 @@
import * as monthUtils from '../../shared/months'; import * as monthUtils from '../../shared/months';
import { safeNumber } from '../../shared/util';
import * as db from '../db'; import * as db from '../db';
import * as prefs from '../prefs'; import * as prefs from '../prefs';
import * as sheet from '../sheet'; import * as sheet from '../sheet';
@ -7,7 +6,7 @@ import { batchMessages } from '../sync';
async function getSheetValue(sheetName, cell) { async function getSheetValue(sheetName, cell) {
const node = await sheet.getCell(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 // 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 }) { export function setBudget({ category, month, amount }) {
amount = safeNumber(typeof amount === 'number' ? amount : 0); if (typeof amount !== 'number') {
amount = 0;
}
const table = getBudgetTable(); const table = getBudgetTable();
let existing = db.firstSync( let existing = db.firstSync(
@ -184,12 +185,32 @@ export async function set3MonthAvg({ month }) {
'sum-amount-' + cat.id '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 }); 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 }) { export async function holdForNextMonth({ month, amount }) {
let row = await db.first( let row = await db.first(
'SELECT buffered FROM zero_budget_months WHERE id = ?', 'SELECT buffered FROM zero_budget_months WHERE id = ?',
@ -212,6 +233,18 @@ export async function holdForNextMonth({ month, amount }) {
return false; 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 }) { export async function resetHold({ month }) {
await setBuffer(month, 0); 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-zero', mutator(undoable(actions.setZero)));
app.method('budget/set-3month-avg', mutator(undoable(actions.set3MonthAvg))); app.method('budget/set-3month-avg', mutator(undoable(actions.set3MonthAvg)));
app.method('budget/set-all-future', mutator(undoable(actions.setAllFuture)));
app.method( app.method(
'budget/hold-for-next-month', 'budget/hold-for-next-month',
mutator(undoable(actions.holdForNextMonth)) 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/reset-hold', mutator(undoable(actions.resetHold)));
app.method( app.method(
'budget/cover-overspending', 'budget/cover-overspending',

View file

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

View file

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

View file

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

View file

@ -22,7 +22,7 @@ export function keyToTimestamp(key) {
export function insert(trie, timestamp) { export function insert(trie, timestamp) {
let hash = timestamp.hash(); 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 }); trie = Object.assign({}, trie, { hash: trie.hash ^ hash });
return insertKey(trie, key, hash); return insertKey(trie, key, hash);

View file

@ -34,7 +34,7 @@ export function shoveSortOrders(items, targetId) {
} else { } else {
if (target.sort_order - (before ? before.sort_order : 0) <= 2) { if (target.sort_order - (before ? before.sort_order : 0) <= 2) {
let next = to; 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) { while (next < items.length) {
// No need to update it if it's already greater than the current // No need to update it if it's already greater than the current
// order. This can happen because there may already be large // 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 shuffled = new Array(src.length);
let item; let item;
while ((item = src.pop())) { while ((item = src.pop())) {
let idx = Math.floor(Math.random() * shuffled.length); let idx = (Math.random() * shuffled.length) | 0;
if (shuffled[idx]) { if (shuffled[idx]) {
src.push(item); src.push(item);
} else { } else {

View file

@ -199,5 +199,5 @@ export function makeValue(value, cond) {
} }
export function getApproxNumberThreshold(number) { 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) { export function getScheduledAmount(amount) {
if (amount && typeof amount !== 'number') { if (amount && typeof amount !== 'number') {
return Math.round((amount.num1 + amount.num2) / 2); return ((amount.num1 + amount.num2) / 2) | 0;
} }
return amount; return amount;
} }

View file

@ -298,30 +298,6 @@ export function getNumberFormat() {
setNumberFormat('comma-dot'); 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) { export function toRelaxedNumber(value) {
return integerToAmount(currencyToInteger(value) || 0); return integerToAmount(currencyToInteger(value) || 0);
} }
@ -331,7 +307,8 @@ export function toRelaxedInteger(value) {
} }
export function integerToCurrency(n) { export function integerToCurrency(n) {
return numberFormat.formatter.format(safeNumber(n) / 100); // Awesome
return numberFormat.formatter.format(n / 100);
} }
export function amountToCurrency(n) { export function amountToCurrency(n) {
@ -363,7 +340,7 @@ export function amountToInteger(n) {
} }
export function integerToAmount(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 // 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', name: 'set-3-avg',
text: 'Set budgets to 3 month 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', name: 'buffer',
text: 'Hold for next month' text: 'Hold for next month'
}, },
{
name: 'buffer-future',
text: 'Hold for all future months'
},
{ {
name: 'reset-buffer', name: 'reset-buffer',
text: "Reset next month's 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' && ( {state.menuOpen === 'transfer' && (
<TransferTooltip <TransferTooltip
initialAmountName="leftover" initialAmountName="leftover"

View file

@ -938,6 +938,10 @@ export function ModalButtons({
style 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} {leftContent}
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
{children} {children}

View file

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

View file

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

View file

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

View file

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

View file

@ -24,11 +24,11 @@ global.Date.now = () => 123456789;
let seqId = 1; let seqId = 1;
uuid.v4 = function() { 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() { uuid.v4Sync = function() {
return 'testing-uuid-' + Math.floor(Math.random() * 1000000); return 'testing-uuid-' + ((Math.random() * 1000000) | 0);
}; };
global.__resetWorld = () => { 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: case 3:
this.onBudgetAction('set-3-avg'); this.onBudgetAction('set-3-avg');
break; break;
case 4:
if (budgetType === 'report') {
this.onBudgetAction('set-all-future');
break;
}
default: default:
} }
} }