Merge pull request #38247 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-11-22 09:43:45 +05:30
committed by GitHub
43 changed files with 618 additions and 281 deletions

View File

@@ -68,7 +68,12 @@
"enable_party_matching", "enable_party_matching",
"enable_fuzzy_matching", "enable_fuzzy_matching",
"tab_break_dpet", "tab_break_dpet",
"show_balance_in_coa" "show_balance_in_coa",
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"column_break_lvjk",
"receivable_payable_remarks_length"
], ],
"fields": [ "fields": [
{ {
@@ -429,6 +434,34 @@
"fieldname": "show_balance_in_coa", "fieldname": "show_balance_in_coa",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts" "label": "Show Balances in Chart Of Accounts"
},
{
"fieldname": "reports_tab",
"fieldtype": "Tab Break",
"label": "Reports"
},
{
"default": "0",
"description": "Truncates 'Remarks' column to set character length",
"fieldname": "general_ledger_remarks_length",
"fieldtype": "Int",
"label": "General Ledger"
},
{
"default": "0",
"description": "Truncates 'Remarks' column to set character length",
"fieldname": "receivable_payable_remarks_length",
"fieldtype": "Int",
"label": "Accounts Receivable/Payable"
},
{
"fieldname": "column_break_lvjk",
"fieldtype": "Column Break"
},
{
"fieldname": "remarks_section",
"fieldtype": "Section Break",
"label": "Remarks Column Length"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -436,7 +469,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-07-27 15:05:34.000264", "modified": "2023-11-20 09:37:47.650347",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -829,7 +829,6 @@ frappe.ui.form.on('Payment Entry', {
else else
total_negative_outstanding += Math.abs(flt(row.outstanding_amount)); total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
}) })
var allocated_negative_outstanding = 0; var allocated_negative_outstanding = 0;
if ( if (
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
@@ -844,6 +843,7 @@ frappe.ui.form.on('Payment Entry', {
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding; var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) { } else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"))
if(paid_amount > total_negative_outstanding) { if(paid_amount > total_negative_outstanding) {
if(total_negative_outstanding == 0) { if(total_negative_outstanding == 0) {
frappe.msgprint( frappe.msgprint(

View File

@@ -913,8 +913,11 @@ class PaymentEntry(AccountsController):
): ):
return return
total_negative_outstanding = sum( total_negative_outstanding = flt(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
),
self.references[0].precision("outstanding_amount") if self.references else None,
) )
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount

View File

@@ -1137,6 +1137,40 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pay.unallocated_amount, 1000) self.assertEqual(pay.unallocated_amount, 1000)
self.assertEqual(pay.difference_amount, 0) self.assertEqual(pay.difference_amount, 0)
def test_rounding_of_unallocated_amount(self):
self.supplier = "_Test Supplier USD"
pi = self.create_purchase_invoice(qty=1, rate=10, do_not_submit=True)
pi.supplier = self.supplier
pi.currency = "USD"
pi.conversion_rate = 80
pi.credit_to = self.creditors_usd
pi.save().submit()
pe = get_payment_entry(pi.doctype, pi.name)
pe.target_exchange_rate = 78.726500000
pe.received_amount = 26.75
pe.paid_amount = 2105.93
pe.references = []
pe.save().submit()
# unallocated_amount will have some rounding loss - 26.749950
self.assertNotEqual(pe.unallocated_amount, 26.75)
pr = frappe.get_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Supplier"
pr.party = self.supplier
pr.receivable_payable_account = self.creditors_usd
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
pr.get_unreconciled_entries()
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
pr.reconcile()
def make_customer(customer_name, currency=None): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):

View File

@@ -18,6 +18,7 @@
"is_pos", "is_pos",
"is_return", "is_return",
"update_billed_amount_in_sales_order", "update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"column_break1", "column_break1",
"company", "company",
"posting_date", "posting_date",
@@ -1549,12 +1550,19 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount Eligible for Commission", "label": "Amount Eligible for Commission",
"read_only": 1 "read_only": 1
},
{
"default": "1",
"depends_on": "eval: doc.is_return && doc.return_against",
"fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-09-30 03:49:50.455199", "modified": "2023-11-20 12:27:12.848149",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -2147,7 +2147,7 @@
"label": "Use Company default Cost Center for Round off" "label": "Use Company default Cost Center for Round off"
}, },
{ {
"default": "0", "default": "1",
"depends_on": "eval: doc.is_return", "depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_delivery_note", "fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check", "fieldtype": "Check",
@@ -2164,7 +2164,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-11-03 14:39:38.012346", "modified": "2023-11-20 11:51:43.555197",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -782,6 +782,28 @@ class TestSalesInvoice(FrappeTestCase):
w = self.make() w = self.make()
self.assertEqual(w.outstanding_amount, w.base_rounded_total) self.assertEqual(w.outstanding_amount, w.base_rounded_total)
def test_rounded_total_with_cash_discount(self):
si = frappe.copy_doc(test_records[2])
item = copy.deepcopy(si.get("items")[0])
item.update(
{
"qty": 1,
"rate": 14960.66,
}
)
si.set("items", [item])
si.set("taxes", [])
si.apply_discount_on = "Grand Total"
si.is_cash_or_non_trade_discount = 1
si.discount_amount = 1
si.insert()
self.assertEqual(si.grand_total, 14959.66)
self.assertEqual(si.rounded_total, 14960)
self.assertEqual(si.rounding_adjustment, 0.34)
def test_payment(self): def test_payment(self):
w = self.make() w = self.make()

View File

@@ -31,7 +31,12 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
from erpnext.utilities.regional import temporary_flag from erpnext.utilities.regional import temporary_flag
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"} PURCHASE_TRANSACTION_TYPES = {
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
}
SALES_TRANSACTION_TYPES = { SALES_TRANSACTION_TYPES = {
"Quotation", "Quotation",
"Sales Order", "Sales Order",
@@ -231,7 +236,9 @@ def set_address_details(
if shipping_address: if shipping_address:
party_details.update( party_details.update(
shipping_address=shipping_address, shipping_address=shipping_address,
shipping_address_display=render_address(shipping_address), shipping_address_display=render_address(
shipping_address, check_permissions=not ignore_permissions
),
**get_fetch_values(doctype, "shipping_address", shipping_address) **get_fetch_values(doctype, "shipping_address", shipping_address)
) )

View File

@@ -7,7 +7,7 @@ from collections import OrderedDict
import frappe import frappe
from frappe import _, qb, scrub from frappe import _, qb, scrub
from frappe.query_builder import Criterion from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Sum from frappe.query_builder.functions import Date, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -762,7 +762,12 @@ class ReceivablePayableReport(object):
) )
if self.filters.get("show_remarks"): if self.filters.get("show_remarks"):
query = query.select(ple.remarks) if remarks_length := frappe.db.get_single_value(
"Accounts Settings", "receivable_payable_remarks_length"
):
query = query.select(Substring(ple.remarks, 1, remarks_length).as_("remarks"))
else:
query = query.select(ple.remarks)
if self.filters.get("group_by_party"): if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date) query = query.orderby(self.ple.party, self.ple.posting_date)

View File

@@ -164,7 +164,12 @@ def get_gl_entries(filters, accounting_dimensions):
credit_in_account_currency """ credit_in_account_currency """
if filters.get("show_remarks"): if filters.get("show_remarks"):
select_fields += """,remarks""" if remarks_length := frappe.db.get_single_value(
"Accounts Settings", "general_ledger_remarks_length"
):
select_fields += f",substr(remarks, 1, {remarks_length}) as 'remarks'"
else:
select_fields += """,remarks"""
order_by_statement = "order by posting_date, account, creation" order_by_statement = "order by posting_date, account, creation"

View File

@@ -184,6 +184,16 @@ def get_columns(filters):
"width": 180, "width": 180,
} }
) )
else:
columns.append(
{
"label": _(filters.get("party_type")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
}
)
columns.extend( columns.extend(
[ [

View File

@@ -533,7 +533,7 @@ def check_if_advance_entry_modified(args):
where where
name = %(voucher_no)s and docstatus = 1 name = %(voucher_no)s and docstatus = 1
and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s
and round(unallocated_amount, {1}) = %(unreconciled_amount)s and round(unallocated_amount, {1}) = round(%(unreconciled_amount)s, {1})
""".format( """.format(
party_account_field, precision party_account_field, precision
), ),

View File

@@ -495,6 +495,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"default": "1",
"fieldname": "asset_quantity", "fieldname": "asset_quantity",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Asset Quantity", "label": "Asset Quantity",
@@ -564,7 +565,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2023-11-15 17:40:17.315203", "modified": "2023-11-20 21:05:45.216899",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@@ -1221,6 +1221,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
"expected_value_after_useful_life": flt(gross_purchase_amount) "expected_value_after_useful_life": flt(gross_purchase_amount)
* flt(d.salvage_value_percentage / 100), * flt(d.salvage_value_percentage / 100),
"depreciation_start_date": d.depreciation_start_date or nowdate(), "depreciation_start_date": d.depreciation_start_date or nowdate(),
"rate_of_depreciation": d.rate_of_depreciation,
} }
) )

View File

@@ -1,30 +1,21 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Bulk Transaction Log', { frappe.ui.form.on("Bulk Transaction Log", {
refresh(frm) {
refresh: function(frm) { frm.add_custom_button(__('Succeeded Entries'), function() {
frm.disable_save(); frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Success"});
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{ }, __("View"));
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{ frm.add_custom_button(__('Failed Entries'), function() {
query(frm, 1); frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Failed"});
} }, __("View"));
); if (frm.doc.failed) {
}); frm.add_custom_button(__('Retry Failed Transactions'), function() {
} frappe.call({
method: "erpnext.utilities.bulk_transaction.retry",
args: {date: frm.doc.date}
});
});
}
},
}); });
function query(frm) {
frappe.call({
method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
args: {
log_date: frm.doc.log_date
}
}).then((r) => {
if (r.message === "No Failed Records") {
frappe.show_alert(__(r.message), 5);
} else {
frappe.show_alert(__("Retrying Failed Transactions"), 5);
}
});
}

View File

@@ -1,31 +1,64 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "allow_copy": 1,
"creation": "2021-11-30 13:41:16.343827", "creation": "2023-11-09 20:14:45.139593",
"default_view": "List",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"log_date", "date",
"logger_data" "column_break_bsan",
"log_entries",
"section_break_mdmv",
"succeeded",
"column_break_qryp",
"failed"
], ],
"fields": [ "fields": [
{ {
"fieldname": "log_date", "fieldname": "date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Log Date", "in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "logger_data", "fieldname": "log_entries",
"fieldtype": "Table", "fieldtype": "Int",
"label": "Logger Data", "in_list_view": 1,
"options": "Bulk Transaction Log Detail" "label": "Log Entries",
"read_only": 1
},
{
"fieldname": "column_break_bsan",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_mdmv",
"fieldtype": "Section Break"
},
{
"fieldname": "succeeded",
"fieldtype": "Int",
"label": "Succeeded",
"read_only": 1
},
{
"fieldname": "column_break_qryp",
"fieldtype": "Column Break"
},
{
"fieldname": "failed",
"fieldtype": "Int",
"label": "Failed",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "in_create": 1,
"is_virtual": 1,
"links": [], "links": [],
"modified": "2022-02-03 17:23:02.935325", "modified": "2023-11-11 04:52:49.347376",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Bulk Transaction", "module": "Bulk Transaction",
"name": "Bulk Transaction Log", "name": "Bulk Transaction Log",
@@ -47,5 +80,5 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "title_field": "date"
} }

View File

@@ -1,67 +1,112 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from datetime import date
import frappe import frappe
from frappe import qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.functions import Count
from erpnext.utilities.bulk_transaction import task, update_logger from frappe.utils import cint
from pypika import Order
class BulkTransactionLog(Document): class BulkTransactionLog(Document):
pass def db_insert(self, *args, **kwargs):
pass
def load_from_db(self):
log_detail = qb.DocType("Bulk Transaction Log Detail")
@frappe.whitelist() has_records = frappe.db.sql(
def retry_failing_transaction(log_date=None): f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');"
if not log_date: )[0][0]
log_date = str(date.today()) if not has_records:
btp = frappe.qb.DocType("Bulk Transaction Log Detail") raise frappe.DoesNotExistError
data = (
frappe.qb.from_(btp)
.select(btp.transaction_name, btp.from_doctype, btp.to_doctype)
.distinct()
.where(btp.retried != 1)
.where(btp.transaction_status == "Failed")
.where(btp.date == log_date)
).run(as_dict=True)
if data: succeeded_logs = (
if len(data) > 10: qb.from_(log_detail)
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date) .select(Count(log_detail.date).as_("count"))
else: .where((log_detail.date == self.name) & (log_detail.transaction_status == "Success"))
job(data, log_date) .run()
else: )[0][0] or 0
return "No Failed Records" failed_logs = (
qb.from_(log_detail)
.select(Count(log_detail.date).as_("count"))
.where((log_detail.date == self.name) & (log_detail.transaction_status == "Failed"))
.run()
)[0][0] or 0
total_logs = succeeded_logs + failed_logs
transaction_log = frappe._dict(
{
"date": self.name,
"count": total_logs,
"succeeded": succeeded_logs,
"failed": failed_logs,
}
)
super(Document, self).__init__(serialize_transaction_log(transaction_log))
@staticmethod
def get_list(args):
filter_date = parse_list_filters(args)
limit = cint(args.get("page_length")) or 20
log_detail = qb.DocType("Bulk Transaction Log Detail")
def job(data, log_date): dates_query = (
for d in data: qb.from_(log_detail)
failed = [] .select(log_detail.date)
try: .distinct()
frappe.db.savepoint("before_creation_of_record") .orderby(log_detail.date, order=Order.desc)
task(d.transaction_name, d.from_doctype, d.to_doctype) .limit(limit)
except Exception as e: )
frappe.db.rollback(save_point="before_creation_of_record") if filter_date:
failed.append(e) dates_query = dates_query.where(log_detail.date == filter_date)
update_logger( dates = dates_query.run()
d.transaction_name,
e, transaction_logs = []
d.from_doctype, if dates:
d.to_doctype, transaction_logs_query = (
status="Failed", qb.from_(log_detail)
log_date=log_date, .select(log_detail.date.as_("date"), Count(log_detail.date).as_("count"))
restarted=1, .where(log_detail.date.isin(dates))
.orderby(log_detail.date, order=Order.desc)
.groupby(log_detail.date)
.limit(limit)
) )
transaction_logs = transaction_logs_query.run(as_dict=True)
if not failed: return [serialize_transaction_log(x) for x in transaction_logs]
update_logger(
d.transaction_name, @staticmethod
None, def get_count(args):
d.from_doctype, pass
d.to_doctype,
status="Success", @staticmethod
log_date=log_date, def get_stats(args):
restarted=1, pass
)
def db_update(self, *args, **kwargs):
pass
def delete(self):
pass
def serialize_transaction_log(data):
return frappe._dict(
name=data.date,
date=data.date,
log_entries=data.count,
succeeded=data.succeeded,
failed=data.failed,
)
def parse_list_filters(args):
# parse date filter
filter_date = None
for fil in args.get("filters"):
if isinstance(fil, list):
for elem in fil:
if elem == "date":
filter_date = fil[3]
return filter_date

View File

@@ -1,79 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest # import frappe
from datetime import date from frappe.tests.utils import FrappeTestCase
import frappe
from erpnext.utilities.bulk_transaction import transaction_processing
class TestBulkTransactionLog(unittest.TestCase): class TestBulkTransactionLog(FrappeTestCase):
def setUp(self): pass
create_company()
create_customer()
create_item()
def test_entry_in_log(self):
so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
doc = frappe.get_doc("Bulk Transaction Log", str(date.today()))
for d in doc.get("logger_data"):
if d.transaction_name == so_name:
self.assertEqual(d.transaction_name, so_name)
self.assertEqual(d.transaction_status, "Success")
self.assertEqual(d.from_doctype, "Sales Order")
self.assertEqual(d.to_doctype, "Sales Invoice")
self.assertEqual(d.retried, 0)
def create_company():
if not frappe.db.exists("Company", "_Test Company"):
frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test Company",
"country": "India",
"default_currency": "INR",
}
).insert()
def create_customer():
if not frappe.db.exists("Customer", "Bulk Customer"):
frappe.get_doc({"doctype": "Customer", "customer_name": "Bulk Customer"}).insert()
def create_item():
if not frappe.db.exists("Item", "MK"):
frappe.get_doc(
{
"doctype": "Item",
"item_code": "MK",
"item_name": "Milk",
"description": "Milk",
"item_group": "Products",
}
).insert()
def create_so(intent=None):
so = frappe.new_doc("Sales Order")
so.customer = "Bulk Customer"
so.company = "_Test Company"
so.transaction_date = date.today()
so.set_warehouse = "Finished Goods - _TC"
so.append(
"items",
{
"item_code": "MK",
"delivery_date": date.today(),
"qty": 10,
"rate": 80,
},
)
so.insert()
so.submit()
return so.name

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Bulk Transaction Log Detail", {
// refresh(frm) {
// },
// });

View File

@@ -6,12 +6,12 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"from_doctype",
"transaction_name", "transaction_name",
"date", "date",
"time", "time",
"transaction_status", "transaction_status",
"error_description", "error_description",
"from_doctype",
"to_doctype", "to_doctype",
"retried" "retried"
], ],
@@ -20,8 +20,11 @@
"fieldname": "transaction_name", "fieldname": "transaction_name",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Name", "label": "Name",
"options": "from_doctype" "options": "from_doctype",
"read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "transaction_status", "fieldname": "transaction_status",
@@ -39,9 +42,11 @@
{ {
"fieldname": "from_doctype", "fieldname": "from_doctype",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1,
"label": "From Doctype", "label": "From Doctype",
"options": "DocType", "options": "DocType",
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "to_doctype", "fieldname": "to_doctype",
@@ -54,8 +59,10 @@
"fieldname": "date", "fieldname": "date",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1,
"label": "Date ", "label": "Date ",
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "time", "fieldname": "time",
@@ -66,19 +73,33 @@
{ {
"fieldname": "retried", "fieldname": "retried",
"fieldtype": "Int", "fieldtype": "Int",
"in_list_view": 1,
"label": "Retried", "label": "Retried",
"read_only": 1 "read_only": 1
} }
], ],
"in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1,
"links": [], "links": [],
"modified": "2022-02-03 19:57:31.650359", "modified": "2023-11-10 11:44:10.758342",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Bulk Transaction", "module": "Bulk Transaction",
"name": "Bulk Transaction Log Detail", "name": "Bulk Transaction Log Detail",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestBulkTransactionLogDetail(FrappeTestCase):
pass

View File

@@ -9,6 +9,8 @@
"field_order": [ "field_order": [
"naming_series", "naming_series",
"company", "company",
"billing_address",
"billing_address_display",
"vendor", "vendor",
"column_break1", "column_break1",
"transaction_date", "transaction_date",
@@ -292,13 +294,25 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Send Document Print", "label": "Send Document Print",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "billing_address",
"fieldtype": "Link",
"label": "Company Billing Address",
"options": "Address"
},
{
"fieldname": "billing_address_display",
"fieldtype": "Small Text",
"label": "Billing Address Details",
"read_only": 1
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-08-09 12:20:26.850623", "modified": "2023-11-06 12:45:28.898706",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",

View File

@@ -20,6 +20,10 @@
"valid_till", "valid_till",
"quotation_number", "quotation_number",
"amended_from", "amended_from",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@@ -79,6 +83,7 @@
"pricing_rule_details", "pricing_rule_details",
"pricing_rules", "pricing_rules",
"address_and_contact_tab", "address_and_contact_tab",
"supplier_address_section",
"supplier_address", "supplier_address",
"address_display", "address_display",
"column_break_72", "column_break_72",
@@ -86,6 +91,14 @@
"contact_display", "contact_display",
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
"shipping_address_section",
"shipping_address",
"column_break_zjaq",
"shipping_address_display",
"company_billing_address_section",
"billing_address",
"column_break_gcth",
"billing_address_display",
"terms_tab", "terms_tab",
"tc_name", "tc_name",
"terms", "terms",
@@ -837,6 +850,76 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Shipping Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "column_break_zjaq",
"fieldtype": "Column Break"
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Small Text",
"label": "Shipping Address Details",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "shipping_address_section",
"fieldtype": "Section Break",
"label": "Shipping Address"
},
{
"fieldname": "supplier_address_section",
"fieldtype": "Section Break",
"label": "Supplier Address"
},
{
"fieldname": "company_billing_address_section",
"fieldtype": "Section Break",
"label": "Company Billing Address"
},
{
"fieldname": "billing_address",
"fieldtype": "Link",
"label": "Company Billing Address",
"options": "Address"
},
{
"fieldname": "column_break_gcth",
"fieldtype": "Column Break"
},
{
"fieldname": "billing_address_display",
"fieldtype": "Small Text",
"label": "Billing Address Details",
"read_only": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
@@ -844,7 +927,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-14 16:43:41.714832", "modified": "2023-11-20 11:15:30.083077",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@@ -68,6 +68,8 @@
"column_break_15", "column_break_15",
"manufacturer_part_no", "manufacturer_part_no",
"ad_sec_break", "ad_sec_break",
"cost_center",
"dimension_col_break",
"project", "project",
"section_break_44", "section_break_44",
"page_break" "page_break"
@@ -555,13 +557,23 @@
"fieldname": "expected_delivery_date", "fieldname": "expected_delivery_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Expected Delivery Date" "label": "Expected Delivery Date"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-11-14 18:35:03.435817", "modified": "2023-11-17 12:25:26.235367",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation Item", "name": "Supplier Quotation Item",

View File

@@ -87,7 +87,8 @@ class BuyingController(SubcontractingController):
"posting_date": self.get("posting_date"), "posting_date": self.get("posting_date"),
"posting_time": self.get("posting_time"), "posting_time": self.get("posting_time"),
"qty": row.qty, "qty": row.qty,
"serial_and_batch_bundle": row.get("serial_and_batch_bundle"), "serial_no": row.serial_no,
"batch_no": row.batch_no,
"company": self.company, "company": self.company,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
@@ -745,7 +746,7 @@ class BuyingController(SubcontractingController):
"calculate_depreciation": 1, "calculate_depreciation": 1,
"purchase_receipt_amount": purchase_amount, "purchase_receipt_amount": purchase_amount,
"gross_purchase_amount": purchase_amount, "gross_purchase_amount": purchase_amount,
"asset_quantity": row.qty if is_grouped_asset else 0, "asset_quantity": row.qty if is_grouped_asset else 1,
"purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
"purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
"cost_center": row.cost_center, "cost_center": row.cost_center,

View File

@@ -356,6 +356,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice": if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
doc.consolidated_invoice = "" doc.consolidated_invoice = ""
doc.set("payments", []) doc.set("payments", [])
doc.update_billed_amount_in_delivery_note = True
for data in source.payments: for data in source.payments:
paid_amount = 0.00 paid_amount = 0.00
base_paid_amount = 0.00 base_paid_amount = 0.00

View File

@@ -51,6 +51,7 @@ class calculate_taxes_and_totals(object):
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount self.doc.grand_total -= self.doc.discount_amount
self.doc.base_grand_total -= self.doc.base_discount_amount self.doc.base_grand_total -= self.doc.base_discount_amount
self.doc.rounding_adjustment = self.doc.base_rounding_adjustment = 0.0
self.set_rounded_total() self.set_rounded_total()
self.calculate_shipping_charges() self.calculate_shipping_charges()

View File

@@ -424,7 +424,7 @@ scheduler_events = {
"hourly_long": [ "hourly_long": [
"erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.accounts.doctype.subscription.subscription.process_all",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", "erpnext.utilities.bulk_transaction.retry",
], ],
"daily": [ "daily": [
"erpnext.support.doctype.issue.issue.auto_close_tickets", "erpnext.support.doctype.issue.issue.auto_close_tickets",
@@ -548,6 +548,8 @@ accounting_dimension_doctypes = [
"Subcontracting Receipt", "Subcontracting Receipt",
"Subcontracting Receipt Item", "Subcontracting Receipt Item",
"Account Closing Balance", "Account Closing Balance",
"Supplier Quotation",
"Supplier Quotation Item",
] ]
# get matching queries for Bank Reconciliation # get matching queries for Bank Reconciliation

View File

@@ -349,5 +349,7 @@ execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50
erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month
erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based
erpnext.patches.v14_0.add_default_for_repost_settings erpnext.patches.v14_0.add_default_for_repost_settings
erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
erpnext.patches.v14_0.update_zero_asset_quantity_field
# below migration patch should always run last # below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger

View File

@@ -0,0 +1,8 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
create_accounting_dimensions_for_doctype,
)
def execute():
create_accounting_dimensions_for_doctype(doctype="Supplier Quotation")
create_accounting_dimensions_for_doctype(doctype="Supplier Quotation Item")

View File

@@ -0,0 +1,6 @@
import frappe
def execute():
asset = frappe.qb.DocType("Asset")
frappe.qb.update(asset).set(asset.asset_quantity, 1).where(asset.asset_quantity == 0).run()

View File

@@ -65,6 +65,12 @@ class Timesheet(Document):
if args.is_billable: if args.is_billable:
if flt(args.billing_hours) == 0.0: if flt(args.billing_hours) == 0.0:
args.billing_hours = args.hours args.billing_hours = args.hours
elif flt(args.billing_hours) > flt(args.hours):
frappe.msgprint(
_("Warning - Row {0}: Billing Hours are more than Actual Hours").format(args.idx),
indicator="orange",
alert=True,
)
else: else:
args.billing_hours = 0 args.billing_hours = 0

View File

@@ -43,6 +43,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if (this.frm.doc.apply_discount_on == "Grand Total" && this.frm.doc.is_cash_or_non_trade_discount) { if (this.frm.doc.apply_discount_on == "Grand Total" && this.frm.doc.is_cash_or_non_trade_discount) {
this.frm.doc.grand_total -= this.frm.doc.discount_amount; this.frm.doc.grand_total -= this.frm.doc.discount_amount;
this.frm.doc.base_grand_total -= this.frm.doc.base_discount_amount; this.frm.doc.base_grand_total -= this.frm.doc.base_discount_amount;
this.frm.doc.rounding_adjustment = 0;
this.frm.doc.base_rounding_adjustment = 0;
this.set_rounded_total();
} }
await this.calculate_shipping_charges(); await this.calculate_shipping_charges();

View File

@@ -4,7 +4,7 @@
frappe.provide("erpnext.utils"); frappe.provide("erpnext.utils");
const SALES_DOCTYPES = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']; const SALES_DOCTYPES = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice'];
const PURCHASE_DOCTYPES = ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']; const PURCHASE_DOCTYPES = ['Supplier Quotation','Purchase Order', 'Purchase Receipt', 'Purchase Invoice'];
erpnext.utils.get_party_details = function(frm, method, args, callback) { erpnext.utils.get_party_details = function(frm, method, args, callback) {
if (!method) { if (!method) {

View File

@@ -81,8 +81,10 @@ frappe.ui.form.on("Employee", {
employee: frm.doc.name, employee: frm.doc.name,
email: frm.doc.prefered_email email: frm.doc.prefered_email
}, },
freeze: true,
freeze_message: __("Creating User..."),
callback: function (r) { callback: function (r) {
frm.set_value("user_id", r.message); frm.reload_doc();
} }
}); });
} }

View File

@@ -49,6 +49,9 @@ class Employee(NestedSet):
else: else:
existing_user_id = frappe.db.get_value("Employee", self.name, "user_id") existing_user_id = frappe.db.get_value("Employee", self.name, "user_id")
if existing_user_id: if existing_user_id:
user = frappe.get_doc("User", existing_user_id)
validate_employee_role(user, ignore_emp_check=True)
user.save(ignore_permissions=True)
remove_user_permission("Employee", self.name, existing_user_id) remove_user_permission("Employee", self.name, existing_user_id)
def after_rename(self, old, new, merge): def after_rename(self, old, new, merge):
@@ -254,12 +257,26 @@ class Employee(NestedSet):
frappe.cache().hdel("employees_with_number", prev_number) frappe.cache().hdel("employees_with_number", prev_number)
def validate_employee_role(doc, method): def validate_employee_role(doc, method=None, ignore_emp_check=False):
# called via User hook # called via User hook
if "Employee" in [d.role for d in doc.get("roles")]: if not ignore_emp_check:
if not frappe.db.get_value("Employee", {"user_id": doc.name}): if frappe.db.get_value("Employee", {"user_id": doc.name}):
frappe.msgprint(_("Please set User ID field in an Employee record to set Employee Role")) return
doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0])
user_roles = [d.role for d in doc.get("roles")]
if "Employee" in user_roles:
frappe.msgprint(
_("User {0}: Removed Employee role as there is no mapped employee.").format(doc.name)
)
doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0])
if "Employee Self Service" in user_roles:
frappe.msgprint(
_("User {0}: Removed Employee Self Service role as there is no mapped employee.").format(
doc.name
)
)
doc.get("roles").remove(doc.get("roles", {"role": "Employee Self Service"})[0])
def update_user_permissions(doc, method): def update_user_permissions(doc, method):
@@ -371,6 +388,8 @@ def create_user(employee, user=None, email=None):
} }
) )
user.insert() user.insert()
emp.user_id = user.name
emp.save()
return user.name return user.name

View File

@@ -25,6 +25,15 @@ class TestEmployee(unittest.TestCase):
employee1_doc.status = "Left" employee1_doc.status = "Left"
self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save) self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save)
def test_user_has_employee(self):
employee = make_employee("test_emp_user_creation@company.com")
employee_doc = frappe.get_doc("Employee", employee)
user = employee_doc.user_id
self.assertTrue("Employee" in frappe.get_roles(user))
employee_doc.user_id = ""
employee_doc.save()
self.assertTrue("Employee" not in frappe.get_roles(user))
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()

View File

@@ -103,15 +103,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Closing Stock Balance",
"print_hide": 1,
"read_only": 1
},
{ {
"fieldname": "include_uom", "fieldname": "include_uom",
"fieldtype": "Link", "fieldtype": "Link",

View File

@@ -505,7 +505,7 @@
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 1, "hidden": 1,
"label": "Variant Attributes", "label": "Variant Attributes",
"mandatory_depends_on": "has_variants", "mandatory_depends_on": "eval:(doc.has_variants || doc.variant_of) && doc.variant_based_on==='Item Attribute'",
"options": "Item Variant Attribute" "options": "Item Variant Attribute"
}, },
{ {
@@ -897,7 +897,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-09-11 13:46:32.688051", "modified": "2023-09-18 15:41:32.688051",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@@ -732,12 +732,18 @@ class PurchaseReceipt(BuyingController):
def update_assets(self, item, valuation_rate): def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all( assets = frappe.db.get_all(
"Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} "Asset",
filters={"purchase_receipt": self.name, "item_code": item.item_code},
fields=["name", "asset_quantity"],
) )
for asset in assets: for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(valuation_rate)) frappe.db.set_value(
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate)) "Asset", asset.name, "gross_purchase_amount", flt(valuation_rate) * asset.asset_quantity
)
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate) * asset.asset_quantity
)
def update_status(self, status): def update_status(self, status):
self.set_status(update=True, status=status) self.set_status(update=True, status=status)

View File

@@ -202,7 +202,7 @@ def get_valuation_rate():
bin_data = ( bin_data = (
frappe.qb.from_(bin) frappe.qb.from_(bin)
.select( .select(
bin.item_code, Sum(bin.actual_qty * bin.valuation_rate) / Sum(bin.actual_qty).as_("val_rate") bin.item_code, (Sum(bin.actual_qty * bin.valuation_rate) / Sum(bin.actual_qty)).as_("val_rate")
) )
.where(bin.actual_qty > 0) .where(bin.actual_qty > 0)
.groupby(bin.item_code) .groupby(bin.item_code)

View File

@@ -11,6 +11,7 @@
"naming_series", "naming_series",
"supplier", "supplier",
"supplier_name", "supplier_name",
"supplier_delivery_note",
"column_break1", "column_break1",
"company", "company",
"posting_date", "posting_date",
@@ -625,12 +626,17 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Edit Posting Date and Time", "label": "Edit Posting Date and Time",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "supplier_delivery_note",
"fieldtype": "Data",
"label": "Supplier Delivery Note"
} }
], ],
"in_create": 1, "in_create": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-07-06 18:44:16.171842", "modified": "2023-11-16 13:04:00.710534",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt", "name": "Subcontracting Receipt",

View File

@@ -3,6 +3,7 @@ from datetime import date, datetime
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import get_link_to_form, today
@frappe.whitelist() @frappe.whitelist()
@@ -28,6 +29,51 @@ def transaction_processing(data, from_doctype, to_doctype):
job(deserialized_data, from_doctype, to_doctype) job(deserialized_data, from_doctype, to_doctype)
@frappe.whitelist()
def retry(date: str | None = None):
if not date:
date = today()
if date:
failed_docs = frappe.db.get_all(
"Bulk Transaction Log Detail",
filters={"date": date, "transaction_status": "Failed", "retried": 0},
fields=["name", "transaction_name", "from_doctype", "to_doctype"],
)
if not failed_docs:
frappe.msgprint(_("There are no Failed transactions"))
else:
job = frappe.enqueue(
retry_failed_transactions,
failed_docs=failed_docs,
)
frappe.msgprint(
_("Job: {0} has been triggered for processing failed transactions").format(
get_link_to_form("RQ Job", job.id)
)
)
def retry_failed_transactions(failed_docs: list | None):
if failed_docs:
for log in failed_docs:
try:
frappe.db.savepoint("before_creation_state")
task(log.transaction_name, log.from_doctype, log.to_doctype)
except Exception as e:
frappe.db.rollback(save_point="before_creation_state")
update_log(log.name, "Failed", 1, str(frappe.get_traceback()))
else:
update_log(log.name, "Success", 1)
def update_log(log_name, status, retried, err=None):
frappe.db.set_value("Bulk Transaction Log Detail", log_name, "transaction_status", status)
frappe.db.set_value("Bulk Transaction Log Detail", log_name, "retried", retried)
if err:
frappe.db.set_value("Bulk Transaction Log Detail", log_name, "error_description", err)
def job(deserialized_data, from_doctype, to_doctype): def job(deserialized_data, from_doctype, to_doctype):
fail_count = 0 fail_count = 0
for d in deserialized_data: for d in deserialized_data:
@@ -38,7 +84,7 @@ def job(deserialized_data, from_doctype, to_doctype):
except Exception as e: except Exception as e:
frappe.db.rollback(save_point="before_creation_state") frappe.db.rollback(save_point="before_creation_state")
fail_count += 1 fail_count += 1
update_logger( create_log(
doc_name, doc_name,
str(frappe.get_traceback()), str(frappe.get_traceback()),
from_doctype, from_doctype,
@@ -47,7 +93,7 @@ def job(deserialized_data, from_doctype, to_doctype):
log_date=str(date.today()), log_date=str(date.today()),
) )
else: else:
update_logger( create_log(
doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today()) doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today())
) )
@@ -108,45 +154,18 @@ def task(doc_name, from_doctype, to_doctype):
obj.insert(ignore_mandatory=True) obj.insert(ignore_mandatory=True)
def check_logger_doc_exists(log_date): def create_log(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0):
return frappe.db.exists("Bulk Transaction Log", log_date) transaction_log = frappe.new_doc("Bulk Transaction Log Detail")
transaction_log.transaction_name = doc_name
transaction_log.date = today()
def get_logger_doc(log_date):
return frappe.get_doc("Bulk Transaction Log", log_date)
def create_logger_doc():
log_doc = frappe.new_doc("Bulk Transaction Log")
log_doc.set_new_name(set_name=str(date.today()))
log_doc.log_date = date.today()
return log_doc
def append_data_to_logger(log_doc, doc_name, error, from_doctype, to_doctype, status, restarted):
row = log_doc.append("logger_data", {})
row.transaction_name = doc_name
row.date = date.today()
now = datetime.now() now = datetime.now()
row.time = now.strftime("%H:%M:%S") transaction_log.time = now.strftime("%H:%M:%S")
row.transaction_status = status transaction_log.transaction_status = status
row.error_description = str(error) transaction_log.error_description = str(e)
row.from_doctype = from_doctype transaction_log.from_doctype = from_doctype
row.to_doctype = to_doctype transaction_log.to_doctype = to_doctype
row.retried = restarted transaction_log.retried = restarted
transaction_log.save()
def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0):
if not check_logger_doc_exists(log_date):
log_doc = create_logger_doc()
append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted)
log_doc.insert()
else:
log_doc = get_logger_doc(log_date)
if record_exists(log_doc, doc_name, status):
append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted)
log_doc.save()
def show_job_status(fail_count, deserialized_data_count, to_doctype): def show_job_status(fail_count, deserialized_data_count, to_doctype):
@@ -176,23 +195,3 @@ def show_job_status(fail_count, deserialized_data_count, to_doctype):
title="Failed", title="Failed",
indicator="red", indicator="red",
) )
def record_exists(log_doc, doc_name, status):
record = mark_retrired_transaction(log_doc, doc_name)
if record and status == "Failed":
return False
elif record and status == "Success":
return True
else:
return True
def mark_retrired_transaction(log_doc, doc_name):
record = 0
for d in log_doc.get("logger_data"):
if d.transaction_name == doc_name and d.transaction_status == "Failed":
frappe.db.set_value("Bulk Transaction Log Detail", d.name, "retried", 1)
record = record + 1
return record