2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 10:58:59 +00:00

feat: Add provisional mode where submission can be undone

This commit is contained in:
Mildred Ki'Lya 2024-11-20 11:35:58 +01:00
parent a2b80a3414
commit 8ec5e624ed
13 changed files with 260 additions and 5 deletions

View File

@ -223,6 +223,30 @@ export class Doc extends Observable<DocValue | Doc[]> {
return true;
}
get canUndoSubmit() {
if (!this.canCancel) {
return false;
}
if (this.fyo.singles.SystemSettings?.provisionalModeSince == null) {
return false;
}
const submittedAt: Date = (this.submittedAt || this.created) as Date;
if (!submittedAt) {
return false;
}
if (
submittedAt <
(this.fyo.singles.SystemSettings.provisionalModeSince as Date)
) {
return false;
}
return true;
}
get canCancel() {
if (!this.schema.isSubmittable) {
return false;
@ -344,14 +368,17 @@ export class Doc extends Observable<DocValue | Doc[]> {
value?: DocValue | Doc[] | DocValueMap[]
): boolean {
if (fieldname === 'numberSeries' && !this.notInserted) {
// console.log("cannot set %s, numberSeries inserted", fieldname)
return false;
}
if (value === undefined) {
// console.log("cannot set %s, undefined value", fieldname)
return false;
}
if (this.fieldMap[fieldname] === undefined) {
// console.log("cannot set %s, no fieldMap", fieldname, this.fieldMap)
return false;
}
@ -940,12 +967,27 @@ export class Doc extends Observable<DocValue | Doc[]> {
await this.trigger('beforeSubmit');
await this.setAndSync('submitted', true);
await this.setAndSync('submittedAt', new Date());
await this.trigger('afterSubmit');
this.fyo.telemetry.log(Verb.Submitted, this.schemaName);
this.fyo.doc.observer.trigger(`submit:${this.schemaName}`, this.name);
}
async submitUndo() {
if (!this.schema.isSubmittable || !this.submitted || this.cancelled) {
return;
}
await this.trigger('beforeSubmitUndo');
await this.setAndSync('submitted', false);
await this.setAndSync('submittedAt', null);
await this.trigger('afterSubmitUndo');
this.fyo.telemetry.log(Verb.SubmitUndone, this.schemaName);
this.fyo.doc.observer.trigger(`submitUndo:${this.schemaName}`, this.name);
}
async cancel() {
if (!this.schema.isSubmittable || !this.submitted || this.cancelled) {
return;
@ -1058,6 +1100,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
async afterSync() {}
async beforeSubmit() {}
async afterSubmit() {}
async beforeSubmitUndo() {}
async afterSubmitUndo() {}
async beforeRename() {}
async afterRename() {}
async beforeCancel() {}

View File

@ -8,6 +8,7 @@ export enum Verb {
Created = 'created',
Deleted = 'deleted',
Submitted = 'submitted',
SubmitUndone = 'submitUndone',
Cancelled = 'cancelled',
Imported = 'imported',
Exported = 'exported',

View File

@ -56,6 +56,15 @@ export abstract class Transactional extends Doc {
await posting.post();
}
async afterSubmitUndo(): Promise<void> {
await super.afterSubmitUndo();
if (!this.isTransactional) {
return;
}
await this._deletePostings();
}
async afterCancel(): Promise<void> {
await super.afterCancel();
if (!this.isTransactional) {
@ -76,6 +85,10 @@ export abstract class Transactional extends Doc {
return;
}
await this._deletePostings();
}
async _deletePostings(): Promise<void> {
const ledgerEntryIds = (await this.fyo.db.getAll(
ModelNameEnum.AccountingLedgerEntry,
{

View File

@ -245,9 +245,29 @@ export abstract class Invoice extends Transactional {
}
}
async afterSubmitUndo() {
await super.afterSubmitUndo();
await this._cancelPayments({ undo: true });
await this._updatePartyOutStanding();
await this._updateIsItemsReturned();
await this._removeLoyaltyPointEntry();
this.reduceUsedCountOfCoupons();
}
async _undoPayments() {
const paymentIds = await this.getPaymentIds();
for (const paymentId of paymentIds) {
const paymentDoc = (await this.fyo.doc.getDoc(
'Payment',
paymentId
)) as Payment;
await paymentDoc.cancel();
}
}
async afterCancel() {
await super.afterCancel();
await this._cancelPayments();
await this._cancelPayments({ undo: false });
await this._updatePartyOutStanding();
await this._updateIsItemsReturned();
await this._removeLoyaltyPointEntry();
@ -258,14 +278,18 @@ export abstract class Invoice extends Transactional {
await removeLoyaltyPoint(this);
}
async _cancelPayments() {
async _cancelPayments({ undo }: { undo: boolean }) {
const paymentIds = await this.getPaymentIds();
for (const paymentId of paymentIds) {
const paymentDoc = (await this.fyo.doc.getDoc(
'Payment',
paymentId
)) as Payment;
await paymentDoc.cancel();
if (undo) {
await paymentDoc.submitUndo();
} else {
await paymentDoc.cancel();
}
}
}

View File

@ -442,6 +442,11 @@ export class Payment extends Transactional {
}
}
async afterSubmitUndo() {
await super.afterSubmitUndo();
await this.revertOutstandingAmount();
}
async afterCancel() {
await super.afterCancel();
await this.revertOutstandingAmount();

View File

@ -46,6 +46,10 @@ export class StockManager {
await this.#sync();
}
async undoTransfers() {
await this.cancelTransfers();
}
async cancelTransfers() {
const { referenceName, referenceType } = this.details;
await this.fyo.db.deleteAll(ModelNameEnum.StockLedgerEntry, {

View File

@ -69,6 +69,11 @@ export class StockMovement extends Transfer {
await updateSerialNumbers(this, false);
}
async afterSubmitUndo(): Promise<void> {
await super.afterSubmitUndo();
await updateSerialNumbers(this, true);
}
async afterCancel(): Promise<void> {
await super.afterCancel();
await updateSerialNumbers(this, true);

View File

@ -217,6 +217,13 @@ export abstract class StockTransfer extends Transfer {
await this._updateItemsReturned();
}
async afterSubmitUndo() {
await super.afterSubmitUndo();
await updateSerialNumbers(this, false, this.isReturn);
await this._updateBackReference();
await this._updateItemsReturned();
}
async afterCancel(): Promise<void> {
await super.afterCancel();
await updateSerialNumbers(this, true, this.isReturn);

View File

@ -21,6 +21,19 @@ export abstract class Transfer extends Transactional {
await this._getStockManager().createTransfers(transferDetails);
}
async beforeSubmitUndo(): Promise<void> {
await super.beforeSubmitUndo();
const transferDetails = this._getTransferDetails();
const stockManager = this._getStockManager();
stockManager.isCancelled = true;
await stockManager.validateCancel(transferDetails);
}
async afterSubmitUndo(): Promise<void> {
await super.afterSubmitUndo();
await this._getStockManager().undoTransfers();
}
async beforeCancel(): Promise<void> {
await super.beforeCancel();
const transferDetails = this._getTransferDetails();

View File

@ -125,6 +125,13 @@
"default": false,
"description": "Sets the theme of the app.",
"section": "Theme"
},
{
"fieldname": "provisionalModeSince",
"label": "Provisional Mode Since",
"fieldtype": "Datetime",
"description": "Date since the provisional mode is set, or NULL for definitive mode",
"hidden": true
}
],
"quickEditFields": [

View File

@ -9,6 +9,14 @@
"meta": true,
"section": "System"
},
{
"fieldname": "submittedAt",
"label": "Submition Date",
"fieldtype": "Datetime",
"required": false,
"meta": true,
"section": "System"
},
{
"fieldname": "cancelled",
"label": "Cancelled",

View File

@ -109,6 +109,32 @@
<!-- Report Issue and DB Switcher -->
<div class="window-no-drag flex flex-col gap-2 py-2 px-4">
<button
class="
flex
text-sm text-gray-600
dark:text-gray-500
hover:text-gray-800
dark:hover:text-gray-400
gap-1
items-center
"
@click="toggleProvisionalMode"
:title="
isProvisionalMode()
? t`Provisional mode since` + ' ' + provisionalModeDate()
: ''
"
>
<feather-icon
:name="isProvisionalMode() ? 'pause-circle' : 'play-circle'"
class="h-4 w-4 flex-shrink-0"
/>
<p>
{{ isProvisionalMode() ? t`Provisional mode` : t`Definitive mode` }}
</p>
</button>
<button
class="
flex
@ -184,7 +210,12 @@
@click="showDevMode = false"
title="Open dev tools with Ctrl+Shift+I"
>
dev mode
<feather-icon
name="hash"
class="h-4 w-4 flex-shrink-0"
style="display: inline"
/>
hide dev mode
</p>
</div>
@ -214,6 +245,7 @@
</div>
</template>
<script lang="ts">
import { t } from 'fyo';
import { reportIssue } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { languageDirectionKey, shortcutsKey } from 'src/utils/injectionKeys';
@ -226,6 +258,7 @@ import router from '../router';
import Icon from './Icon.vue';
import Modal from './Modal.vue';
import ShortcutsHelper from './ShortcutsHelper.vue';
import { showDialog } from 'src/utils/interactive';
const COMPONENT_NAME = 'Sidebar';
@ -291,6 +324,64 @@ export default defineComponent({
routeTo,
reportIssue,
toggleSidebar,
async toggleProvisionalMode() {
let title, detail, provisionalModeSince, showUnlimited;
if (fyo.singles.SystemSettings?.provisionalModeSince != null) {
title = t`Leave provisional mode?`;
detail = t`All submissions while provisional mode was effective will be definitive.`;
provisionalModeSince = null;
} else {
title = t`Enter provisional mode?`;
detail = t`Documents submission while in provisional mode can be undone.`;
provisionalModeSince = new Date();
showUnlimited = this.showDevMode;
}
let response = (await showDialog({
title,
detail,
type: 'warning',
buttons: [
{
label: t`Yes`,
action() {
return true;
},
isPrimary: true,
},
{
label: t`No`,
action() {
return false;
},
isEscape: true,
},
showUnlimited
? {
label: t`Unlimited`,
action() {
provisionalModeSince = new Date(0);
return true;
},
isPrimary: false,
}
: null,
].filter((x) => x),
})) as boolean;
if (response) {
await fyo.singles.SystemSettings?.setAndSync(
'provisionalModeSince',
provisionalModeSince
);
}
},
isProvisionalMode() {
return fyo.singles.SystemSettings?.provisionalModeSince != null;
},
provisionalModeDate() {
return fyo.singles.SystemSettings?.provisionalModeSince;
},
openDocumentation() {
ipc.openLink('https://docs.frappe.io/' + docsPathRef.value);
},

View File

@ -193,6 +193,7 @@ export function getActionsForDoc(doc?: Doc): Action[] {
...getActions(doc),
getDuplicateAction(doc),
getDeleteAction(doc),
getSubmitUndoAction(doc),
getCancelAction(doc),
];
@ -234,6 +235,19 @@ export function getGroupedActionsForDoc(doc?: Doc): ActionGroup[] {
return [grouped, actionsMap['']].flat().filter(Boolean);
}
function getSubmitUndoAction(doc: Doc): Action {
return {
label: t`Undo Submit`,
component: {
template: '<span class="text-red-700">{{ t`Undo Submit` }}</span>',
},
condition: (doc: Doc) => doc.canUndoSubmit,
async action() {
await commonDocUndoSubmit(doc);
},
};
}
function getCancelAction(doc: Doc): Action {
return {
label: t`Cancel`,
@ -514,6 +528,21 @@ export async function commongDocDelete(
return true;
}
export async function commonDocUndoSubmit(doc: Doc): Promise<boolean> {
let res = false;
try {
res = doc.submitUndo();
} catch (err) {
await handleErrorWithDialog(err as Error, doc);
}
if (!res) {
return false;
}
showActionToast(doc, 'submitUndo');
return true;
}
export async function commonDocCancel(doc: Doc): Promise<boolean> {
const res = await cancelDocWithPrompt(doc);
if (!res) {
@ -726,10 +755,14 @@ function getDocSubmitMessage(doc: Doc): string {
return details.join(' ');
}
function showActionToast(doc: Doc, type: 'sync' | 'cancel' | 'delete') {
function showActionToast(
doc: Doc,
type: 'submitUndo' | 'sync' | 'cancel' | 'delete'
) {
const label = getDocReferenceLabel(doc);
const message = {
sync: t`${label} saved`,
submitUndo: t`${label} submission undone`,
cancel: t`${label} cancelled`,
delete: t`${label} deleted`,
}[type];