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_fuzzy_matching",
"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": [
{
@@ -429,6 +434,34 @@
"fieldname": "show_balance_in_coa",
"fieldtype": "Check",
"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",
@@ -436,7 +469,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-07-27 15:05:34.000264",
"modified": "2023-11-20 09:37:47.650347",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -829,7 +829,6 @@ frappe.ui.form.on('Payment Entry', {
else
total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
})
var allocated_negative_outstanding = 0;
if (
(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;
} 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(total_negative_outstanding == 0) {
frappe.msgprint(

View File

@@ -913,8 +913,11 @@ class PaymentEntry(AccountsController):
):
return
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
total_negative_outstanding = flt(
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

View File

@@ -1137,6 +1137,40 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pay.unallocated_amount, 1000)
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):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -18,6 +18,7 @@
"is_pos",
"is_return",
"update_billed_amount_in_sales_order",
"update_billed_amount_in_delivery_note",
"column_break1",
"company",
"posting_date",
@@ -1549,12 +1550,19 @@
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"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",
"is_submittable": 1,
"links": [],
"modified": "2022-09-30 03:49:50.455199",
"modified": "2023-11-20 12:27:12.848149",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

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

View File

@@ -782,6 +782,28 @@ class TestSalesInvoice(FrappeTestCase):
w = self.make()
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):
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.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 = {
"Quotation",
"Sales Order",
@@ -231,7 +236,9 @@ def set_address_details(
if shipping_address:
party_details.update(
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)
)

View File

@@ -7,7 +7,7 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, scrub
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 erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -762,7 +762,12 @@ class ReceivablePayableReport(object):
)
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"):
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 """
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"

View File

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

View File

@@ -533,7 +533,7 @@ def check_if_advance_entry_modified(args):
where
name = %(voucher_no)s and docstatus = 1
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(
party_account_field, precision
),

View File

@@ -495,6 +495,7 @@
"read_only": 1
},
{
"default": "1",
"fieldname": "asset_quantity",
"fieldtype": "Int",
"label": "Asset Quantity",
@@ -564,7 +565,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2023-11-15 17:40:17.315203",
"modified": "2023-11-20 21:05:45.216899",
"modified_by": "Administrator",
"module": "Assets",
"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)
* flt(d.salvage_value_percentage / 100),
"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
frappe.ui.form.on('Bulk Transaction Log', {
refresh: function(frm) {
frm.disable_save();
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
query(frm, 1);
}
);
});
}
frappe.ui.form.on("Bulk Transaction Log", {
refresh(frm) {
frm.add_custom_button(__('Succeeded Entries'), function() {
frappe.set_route('List', 'Bulk Transaction Log Detail', {'date': frm.doc.date, 'transaction_status': "Success"});
}, __("View"));
frm.add_custom_button(__('Failed Entries'), function() {
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": [],
"allow_rename": 1,
"creation": "2021-11-30 13:41:16.343827",
"allow_copy": 1,
"creation": "2023-11-09 20:14:45.139593",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"log_date",
"logger_data"
"date",
"column_break_bsan",
"log_entries",
"section_break_mdmv",
"succeeded",
"column_break_qryp",
"failed"
],
"fields": [
{
"fieldname": "log_date",
"fieldname": "date",
"fieldtype": "Date",
"label": "Log Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"read_only": 1
},
{
"fieldname": "logger_data",
"fieldtype": "Table",
"label": "Logger Data",
"options": "Bulk Transaction Log Detail"
"fieldname": "log_entries",
"fieldtype": "Int",
"in_list_view": 1,
"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": [],
"modified": "2022-02-03 17:23:02.935325",
"modified": "2023-11-11 04:52:49.347376",
"modified_by": "Administrator",
"module": "Bulk Transaction",
"name": "Bulk Transaction Log",
@@ -47,5 +80,5 @@
"sort_field": "modified",
"sort_order": "DESC",
"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
from datetime import date
import frappe
from frappe import qb
from frappe.model.document import Document
from erpnext.utilities.bulk_transaction import task, update_logger
from frappe.query_builder.functions import Count
from frappe.utils import cint
from pypika import Order
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()
def retry_failing_transaction(log_date=None):
if not log_date:
log_date = str(date.today())
btp = frappe.qb.DocType("Bulk Transaction Log Detail")
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)
has_records = frappe.db.sql(
f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');"
)[0][0]
if not has_records:
raise frappe.DoesNotExistError
if data:
if len(data) > 10:
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
else:
job(data, log_date)
else:
return "No Failed Records"
succeeded_logs = (
qb.from_(log_detail)
.select(Count(log_detail.date).as_("count"))
.where((log_detail.date == self.name) & (log_detail.transaction_status == "Success"))
.run()
)[0][0] or 0
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):
for d in data:
failed = []
try:
frappe.db.savepoint("before_creation_of_record")
task(d.transaction_name, d.from_doctype, d.to_doctype)
except Exception as e:
frappe.db.rollback(save_point="before_creation_of_record")
failed.append(e)
update_logger(
d.transaction_name,
e,
d.from_doctype,
d.to_doctype,
status="Failed",
log_date=log_date,
restarted=1,
dates_query = (
qb.from_(log_detail)
.select(log_detail.date)
.distinct()
.orderby(log_detail.date, order=Order.desc)
.limit(limit)
)
if filter_date:
dates_query = dates_query.where(log_detail.date == filter_date)
dates = dates_query.run()
transaction_logs = []
if dates:
transaction_logs_query = (
qb.from_(log_detail)
.select(log_detail.date.as_("date"), Count(log_detail.date).as_("count"))
.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:
update_logger(
d.transaction_name,
None,
d.from_doctype,
d.to_doctype,
status="Success",
log_date=log_date,
restarted=1,
)
return [serialize_transaction_log(x) for x in transaction_logs]
@staticmethod
def get_count(args):
pass
@staticmethod
def get_stats(args):
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
import unittest
from datetime import date
import frappe
from erpnext.utilities.bulk_transaction import transaction_processing
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestBulkTransactionLog(unittest.TestCase):
def setUp(self):
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
class TestBulkTransactionLog(FrappeTestCase):
pass

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,
"engine": "InnoDB",
"field_order": [
"from_doctype",
"transaction_name",
"date",
"time",
"transaction_status",
"error_description",
"from_doctype",
"to_doctype",
"retried"
],
@@ -20,8 +20,11 @@
"fieldname": "transaction_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Name",
"options": "from_doctype"
"options": "from_doctype",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "transaction_status",
@@ -39,9 +42,11 @@
{
"fieldname": "from_doctype",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "From Doctype",
"options": "DocType",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "to_doctype",
@@ -54,8 +59,10 @@
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Date ",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "time",
@@ -66,19 +73,33 @@
{
"fieldname": "retried",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Retried",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-02-03 19:57:31.650359",
"modified": "2023-11-10 11:44:10.758342",
"modified_by": "Administrator",
"module": "Bulk Transaction",
"name": "Bulk Transaction Log Detail",
"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_order": "DESC",
"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": [
"naming_series",
"company",
"billing_address",
"billing_address_display",
"vendor",
"column_break1",
"transaction_date",
@@ -292,13 +294,25 @@
"fieldtype": "Check",
"label": "Send Document Print",
"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",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-08-09 12:20:26.850623",
"modified": "2023-11-06 12:45:28.898706",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@@ -20,6 +20,10 @@
"valid_till",
"quotation_number",
"amended_from",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -79,6 +83,7 @@
"pricing_rule_details",
"pricing_rules",
"address_and_contact_tab",
"supplier_address_section",
"supplier_address",
"address_display",
"column_break_72",
@@ -86,6 +91,14 @@
"contact_display",
"contact_mobile",
"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",
"tc_name",
"terms",
@@ -837,6 +850,76 @@
"fieldname": "named_place",
"fieldtype": "Data",
"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",
@@ -844,7 +927,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-04-14 16:43:41.714832",
"modified": "2023-11-20 11:15:30.083077",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@@ -68,6 +68,8 @@
"column_break_15",
"manufacturer_part_no",
"ad_sec_break",
"cost_center",
"dimension_col_break",
"project",
"section_break_44",
"page_break"
@@ -555,13 +557,23 @@
"fieldname": "expected_delivery_date",
"fieldtype": "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,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:35:03.435817",
"modified": "2023-11-17 12:25:26.235367",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation Item",

View File

@@ -87,7 +87,8 @@ class BuyingController(SubcontractingController):
"posting_date": self.get("posting_date"),
"posting_time": self.get("posting_time"),
"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,
"voucher_type": self.doctype,
"voucher_no": self.name,
@@ -745,7 +746,7 @@ class BuyingController(SubcontractingController):
"calculate_depreciation": 1,
"purchase_receipt_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_invoice": self.name if self.doctype == "Purchase Invoice" else None,
"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":
doc.consolidated_invoice = ""
doc.set("payments", [])
doc.update_billed_amount_in_delivery_note = True
for data in source.payments:
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"):
self.doc.grand_total -= self.doc.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.calculate_shipping_charges()

View File

@@ -424,7 +424,7 @@ scheduler_events = {
"hourly_long": [
"erpnext.accounts.doctype.subscription.subscription.process_all",
"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": [
"erpnext.support.doctype.issue.issue.auto_close_tickets",
@@ -548,6 +548,8 @@ accounting_dimension_doctypes = [
"Subcontracting Receipt",
"Subcontracting Receipt Item",
"Account Closing Balance",
"Supplier Quotation",
"Supplier Quotation Item",
]
# 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_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.create_accounting_dimensions_in_supplier_quotation
erpnext.patches.v14_0.update_zero_asset_quantity_field
# below migration patch should always run last
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 flt(args.billing_hours) == 0.0:
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:
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) {
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.rounding_adjustment = 0;
this.frm.doc.base_rounding_adjustment = 0;
this.set_rounded_total();
}
await this.calculate_shipping_charges();

View File

@@ -4,7 +4,7 @@
frappe.provide("erpnext.utils");
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) {
if (!method) {

View File

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

View File

@@ -49,6 +49,9 @@ class Employee(NestedSet):
else:
existing_user_id = frappe.db.get_value("Employee", self.name, "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)
def after_rename(self, old, new, merge):
@@ -254,12 +257,26 @@ class Employee(NestedSet):
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
if "Employee" in [d.role for d in doc.get("roles")]:
if not frappe.db.get_value("Employee", {"user_id": doc.name}):
frappe.msgprint(_("Please set User ID field in an Employee record to set Employee Role"))
doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0])
if not ignore_emp_check:
if frappe.db.get_value("Employee", {"user_id": doc.name}):
return
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):
@@ -371,6 +388,8 @@ def create_user(employee, user=None, email=None):
}
)
user.insert()
emp.user_id = user.name
emp.save()
return user.name

View File

@@ -25,6 +25,15 @@ class TestEmployee(unittest.TestCase):
employee1_doc.status = "Left"
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):
frappe.db.rollback()

View File

@@ -103,15 +103,6 @@
"print_hide": 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",
"fieldtype": "Link",
@@ -145,4 +136,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -505,7 +505,7 @@
"fieldtype": "Table",
"hidden": 1,
"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"
},
{
@@ -897,7 +897,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2023-09-11 13:46:32.688051",
"modified": "2023-09-18 15:41:32.688051",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -732,12 +732,18 @@ class PurchaseReceipt(BuyingController):
def update_assets(self, item, valuation_rate):
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:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate))
frappe.db.set_value(
"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):
self.set_status(update=True, status=status)

View File

@@ -202,7 +202,7 @@ def get_valuation_rate():
bin_data = (
frappe.qb.from_(bin)
.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)
.groupby(bin.item_code)

View File

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

View File

@@ -3,6 +3,7 @@ from datetime import date, datetime
import frappe
from frappe import _
from frappe.utils import get_link_to_form, today
@frappe.whitelist()
@@ -28,6 +29,51 @@ def transaction_processing(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):
fail_count = 0
for d in deserialized_data:
@@ -38,7 +84,7 @@ def job(deserialized_data, from_doctype, to_doctype):
except Exception as e:
frappe.db.rollback(save_point="before_creation_state")
fail_count += 1
update_logger(
create_log(
doc_name,
str(frappe.get_traceback()),
from_doctype,
@@ -47,7 +93,7 @@ def job(deserialized_data, from_doctype, to_doctype):
log_date=str(date.today()),
)
else:
update_logger(
create_log(
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)
def check_logger_doc_exists(log_date):
return frappe.db.exists("Bulk Transaction Log", log_date)
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()
def create_log(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0):
transaction_log = frappe.new_doc("Bulk Transaction Log Detail")
transaction_log.transaction_name = doc_name
transaction_log.date = today()
now = datetime.now()
row.time = now.strftime("%H:%M:%S")
row.transaction_status = status
row.error_description = str(error)
row.from_doctype = from_doctype
row.to_doctype = to_doctype
row.retried = restarted
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()
transaction_log.time = now.strftime("%H:%M:%S")
transaction_log.transaction_status = status
transaction_log.error_description = str(e)
transaction_log.from_doctype = from_doctype
transaction_log.to_doctype = to_doctype
transaction_log.retried = restarted
transaction_log.save()
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",
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