diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 83c206aec3d..2ffbd3087f7 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -485,6 +485,10 @@ def set_default_accounts(company): "default_payable_account": frappe.db.get_value( "Account", {"company": company.name, "account_type": "Payable", "is_group": 0} ), + "default_provisional_account": frappe.db.get_value( + "Account", + {"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0}, + ), } ) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 3fc1adff2d3..4a7a57b6275 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -305,6 +305,7 @@ "fieldname": "source_exchange_rate", "fieldtype": "Float", "label": "Exchange Rate", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -334,6 +335,7 @@ "fieldname": "target_exchange_rate", "fieldtype": "Float", "label": "Exchange Rate", + "precision": "9", "print_hide": 1, "reqd": 1 }, @@ -731,7 +733,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-23 20:08:39.559814", + "modified": "2022-12-08 16:25:43.824051", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 51b134a0237..1a761b424ad 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -684,35 +684,34 @@ class PaymentEntry(AccountsController): ) def validate_payment_against_negative_invoice(self): - if (self.payment_type == "Pay" and self.party_type == "Customer") or ( - self.payment_type == "Receive" and self.party_type == "Supplier" + if (self.payment_type != "Pay" or self.party_type != "Customer") and ( + self.payment_type != "Receive" or self.party_type != "Supplier" ): + 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 = sum( + abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0 + ) + + paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount + additional_charges = sum(flt(d.amount) for d in self.deductions) + + if not total_negative_outstanding: + if self.party_type == "Customer": + msg = _("Cannot pay to Customer without any negative outstanding invoice") + else: + msg = _("Cannot receive from Supplier without any negative outstanding invoice") + + frappe.throw(msg, InvalidPaymentEntry) + + elif paid_amount - additional_charges > total_negative_outstanding: + frappe.throw( + _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( + total_negative_outstanding + ), + InvalidPaymentEntry, ) - paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount - additional_charges = sum([flt(d.amount) for d in self.deductions]) - - if not total_negative_outstanding: - frappe.throw( - _("Cannot {0} {1} {2} without any negative outstanding invoice").format( - _(self.payment_type), - (_("to") if self.party_type == "Customer" else _("from")), - self.party_type, - ), - InvalidPaymentEntry, - ) - - elif paid_amount - additional_charges > total_negative_outstanding: - frappe.throw( - _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( - total_negative_outstanding - ), - InvalidPaymentEntry, - ) - def set_title(self): if frappe.flags.in_import and self.title: # do not set title dynamically if title exists during data import. @@ -1188,6 +1187,7 @@ def get_outstanding_reference_documents(args): ple = qb.DocType("Payment Ledger Entry") common_filter = [] + accounting_dimensions_filter = [] posting_and_due_date = [] # confirm that Supplier is not blocked @@ -1217,7 +1217,7 @@ def get_outstanding_reference_documents(args): # Add cost center condition if args.get("cost_center"): condition += " and cost_center='%s'" % args.get("cost_center") - common_filter.append(ple.cost_center == args.get("cost_center")) + accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) date_fields_dict = { "posting_date": ["from_posting_date", "to_posting_date"], @@ -1243,6 +1243,7 @@ def get_outstanding_reference_documents(args): posting_date=posting_and_due_date, min_outstanding=args.get("outstanding_amt_greater_than"), max_outstanding=args.get("outstanding_amt_less_than"), + accounting_dimensions=accounting_dimensions_filter, ) outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) @@ -1639,7 +1640,7 @@ def get_payment_entry( ): reference_doc = None doc = frappe.get_doc(dt, dn) - if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: + if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99: frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) if not party_type: diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 52efd33fefa..ff212f2a35f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -23,6 +23,7 @@ class PaymentReconciliation(Document): def __init__(self, *args, **kwargs): super(PaymentReconciliation, self).__init__(*args, **kwargs) self.common_filter_conditions = [] + self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] @frappe.whitelist() @@ -193,6 +194,7 @@ class PaymentReconciliation(Document): posting_date=self.ple_posting_date_filter, min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None, max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None, + accounting_dimensions=self.accounting_dimension_filter_conditions, ) if self.invoice_limit: @@ -381,7 +383,7 @@ class PaymentReconciliation(Document): self.common_filter_conditions.append(ple.company == self.company) if self.get("cost_center") and (get_invoices or get_return_invoices): - self.common_filter_conditions.append(ple.cost_center == self.cost_center) + self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center) if get_invoices: if self.from_invoice_date: diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index dae029b4084..6030134fff2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -8,6 +8,8 @@ from frappe import qb from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, nowdate +from erpnext import get_default_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account @@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase): self.create_item() self.create_customer() self.create_account() + self.create_cost_center() self.clear_old_entries() def tearDown(self): @@ -216,6 +219,22 @@ class TestPaymentReconciliation(FrappeTestCase): ) return je + def create_cost_center(self): + # Setup cost center + cc_name = "Sub" + + self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company)) + + cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name}) + if cc_exists: + self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name) + else: + sub_cc = frappe.new_doc("Cost Center") + sub_cc.cost_center_name = "Sub" + sub_cc.parent_cost_center = self.main_cc.parent_cost_center + sub_cc.company = self.main_cc.company + self.sub_cc = sub_cc.save() + def test_filter_min_max(self): # check filter condition minimum and maximum amount self.create_sales_invoice(qty=1, rate=300) @@ -578,3 +597,24 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.payments), 1) self.assertEqual(pr.payments[0].amount, amount) self.assertEqual(pr.payments[0].currency, "EUR") + + def test_differing_cost_center_on_invoice_and_payment(self): + """ + Cost Center filter should not affect outstanding amount calculation + """ + + si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True) + si.cost_center = self.main_cc.name + si.submit() + pr = get_payment_entry(si.doctype, si.name) + pr.cost_center = self.sub_cc.name + pr = pr.save().submit() + + pr = self.create_payment_reconciliation() + pr.cost_center = self.main_cc.name + + pr.get_unreconciled_entries() + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 0) + self.assertEqual(len(pr.get("payments")), 0) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index 901ef1987b4..e9139120283 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) { }); } - if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") { + if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") { frm.add_custom_button(__('Create Payment Entry'), function(){ frappe.call({ method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 29c497854cd..e09da678071 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -254,6 +254,7 @@ class PaymentRequest(Document): payment_entry.update( { + "mode_of_payment": self.mode_of_payment, "reference_no": self.name, "reference_date": nowdate(), "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format( @@ -403,25 +404,22 @@ def make_payment_request(**args): else "" ) - existing_payment_request = None - if args.order_type == "Shopping Cart": - existing_payment_request = frappe.db.get_value( - "Payment Request", - {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)}, - ) + draft_payment_request = frappe.db.get_value( + "Payment Request", + {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0}, + ) - if existing_payment_request: + existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) + + if existing_payment_request_amount: + grand_total -= existing_payment_request_amount + + if draft_payment_request: frappe.db.set_value( - "Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False + "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False ) - pr = frappe.get_doc("Payment Request", existing_payment_request) + pr = frappe.get_doc("Payment Request", draft_payment_request) else: - if args.order_type != "Shopping Cart": - existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) - - if existing_payment_request_amount: - grand_total -= existing_payment_request_amount - pr = frappe.new_doc("Payment Request") pr.update( { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 7fa555d0ab9..c5c8d00a292 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -64,12 +64,13 @@ "tax_withholding_net_total", "base_tax_withholding_net_total", "taxes_section", + "tax_category", "taxes_and_charges", "column_break_58", - "tax_category", - "column_break_49", "shipping_rule", + "column_break_49", "incoterm", + "named_place", "section_break_51", "taxes", "totals", @@ -1541,13 +1542,19 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-11-27 16:28:45.559785", + "modified": "2022-12-14 18:37:38.142688", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1611,4 +1618,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 439a891441f..67cd867b97c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -61,12 +61,13 @@ "total", "net_total", "taxes_section", + "tax_category", "taxes_and_charges", "column_break_38", "shipping_rule", - "incoterm", "column_break_55", - "tax_category", + "incoterm", + "named_place", "section_break_40", "taxes", "section_break_43", @@ -2105,6 +2106,12 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-file-text", @@ -2117,7 +2124,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-12-05 16:18:14.532114", + "modified": "2022-12-12 18:34:33.409895", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 30ed91b9744..b834d1404d0 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -121,12 +121,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): else: tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) + cost_center = get_cost_center(inv) + tax_row.update({"cost_center": cost_center}) + if inv.doctype == "Purchase Invoice": return tax_row, tax_deducted_on_advances, voucher_wise_amount else: return tax_row +def get_cost_center(inv): + cost_center = frappe.get_cached_value("Company", inv.company, "cost_center") + + if len(inv.get("taxes", [])) > 0: + cost_center = inv.get("taxes")[0].cost_center + + return cost_center + + def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 75e983afc0e..af706811ab2 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -8,6 +8,7 @@ from frappe.utils import cint, cstr from erpnext.accounts.report.financial_statements import ( get_columns, + get_cost_centers_with_children, get_data, get_filtered_list_for_consolidated_report, get_period_list, @@ -160,10 +161,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) + filters.start_date = start_date + filters.end_date = period["to_date"] + filters.account_type = account_type - amount = get_account_type_based_gl_data( - company, start_date, period["to_date"], account_type, filters - ) + amount = get_account_type_based_gl_data(company, filters) if amount and account_type == "Depreciation": amount *= -1 @@ -175,7 +177,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ return data -def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None): +def get_account_type_based_gl_data(company, filters=None): cond = "" filters = frappe._dict(filters or {}) @@ -191,17 +193,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type, frappe.db.escape(cstr(filters.finance_book)) ) + if filters.get("cost_center"): + filters.cost_center = get_cost_centers_with_children(filters.cost_center) + cond += " and cost_center in %(cost_center)s" + gl_sum = frappe.db.sql_list( """ select sum(credit) - sum(debit) from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s + where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond} + and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond} """.format( cond=cond ), - (company, start_date, end_date, account_type), + filters, ) return gl_sum[0] if gl_sum and gl_sum[0] else 0 diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 6c8f4bb6fe9..560b79243d7 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters): def get_account_type_based_data(account_type, companies, fiscal_year, filters): data = {} total = 0 + filters.account_type = account_type + filters.start_date = fiscal_year.year_start_date + filters.end_date = fiscal_year.year_end_date + for company in companies: - amount = get_account_type_based_gl_data( - company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters - ) + amount = get_account_type_based_gl_data(company, filters) if amount and account_type == "Depreciation": amount *= -1 diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 103c154c5d1..7ba433ea8c8 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -836,6 +836,7 @@ def get_outstanding_invoices( posting_date=None, min_outstanding=None, max_outstanding=None, + accounting_dimensions=None, ): ple = qb.DocType("Payment Ledger Entry") @@ -866,6 +867,7 @@ def get_outstanding_invoices( min_outstanding=min_outstanding, max_outstanding=max_outstanding, get_invoices=True, + accounting_dimensions=accounting_dimensions or [], ) for d in invoice_list: @@ -1615,6 +1617,7 @@ class QueryPaymentLedger(object): .where(ple.delinked == 0) .where(Criterion.all(filter_on_voucher_no)) .where(Criterion.all(self.common_filter)) + .where(Criterion.all(self.dimensions_filter)) .where(Criterion.all(self.voucher_posting_date)) .groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party) ) @@ -1702,6 +1705,7 @@ class QueryPaymentLedger(object): max_outstanding=None, get_payments=False, get_invoices=False, + accounting_dimensions=None, ): """ Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE @@ -1717,6 +1721,7 @@ class QueryPaymentLedger(object): self.reset() self.vouchers = vouchers self.common_filter = common_filter or [] + self.dimensions_filter = accounting_dimensions or [] self.voucher_posting_date = posting_date or [] self.min_outstanding = min_outstanding self.max_outstanding = max_outstanding diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 06fdea030c8..47089f7d850 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -235,11 +235,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e cur_frm.add_custom_button(__('Purchase Invoice'), this.make_purchase_invoice, __('Create')); - if(flt(doc.per_billed)==0 && doc.status != "Delivered") { + if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); } - if(flt(doc.per_billed)==0) { + if(flt(doc.per_billed) < 100) { this.frm.add_custom_button(__('Payment Request'), function() { me.make_payment_request() }, __('Create')); } diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 93496261aa0..ce7de874c56 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -62,12 +62,13 @@ "set_reserve_warehouse", "supplied_items", "taxes_section", + "tax_category", "taxes_and_charges", "column_break_53", - "tax_category", - "column_break_50", "shipping_rule", + "column_break_50", "incoterm", + "named_place", "section_break_52", "taxes", "totals", @@ -1256,13 +1257,19 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-11-17 17:28:07.729943", + "modified": "2022-12-12 18:36:37.455134", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 98c7dc9bd32..a9f5afb2e98 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -57,44 +57,96 @@ frappe.ui.form.on("Request for Quotation",{ }); }, __("Tools")); - frm.add_custom_button(__('Download PDF'), () => { - var suppliers = []; - const fields = [{ - fieldtype: 'Link', - label: __('Select a Supplier'), - fieldname: 'supplier', - options: 'Supplier', - reqd: 1, - get_query: () => { - return { - filters: [ - ["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})] - ] - } - } - }]; - - frappe.prompt(fields, data => { - var child = locals[cdt][cdn] - - var w = window.open( - frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" - +"doctype="+encodeURIComponent(frm.doc.doctype) - +"&name="+encodeURIComponent(frm.doc.name) - +"&supplier="+encodeURIComponent(data.supplier) - +"&no_letterhead=0")); - if(!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + frm.add_custom_button( + __("Download PDF"), + () => { + frappe.prompt( + [ + { + fieldtype: "Link", + label: "Select a Supplier", + fieldname: "supplier", + options: "Supplier", + reqd: 1, + default: frm.doc.suppliers?.length == 1 ? frm.doc.suppliers[0].supplier : "", + get_query: () => { + return { + filters: [ + [ + "Supplier", + "name", + "in", + frm.doc.suppliers.map((row) => { + return row.supplier; + }), + ], + ], + }; + }, + }, + { + fieldtype: "Section Break", + label: "Print Settings", + fieldname: "print_settings", + collapsible: 1, + }, + { + fieldtype: "Link", + label: "Print Format", + fieldname: "print_format", + options: "Print Format", + placeholder: "Standard", + get_query: () => { + return { + filters: { + doc_type: "Request for Quotation", + }, + }; + }, + }, + { + fieldtype: "Link", + label: "Language", + fieldname: "language", + options: "Language", + default: frappe.boot.lang, + }, + { + fieldtype: "Link", + label: "Letter Head", + fieldname: "letter_head", + options: "Letter Head", + default: frm.doc.letter_head, + }, + ], + (data) => { + var w = window.open( + frappe.urllib.get_full_url( + "/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" + + new URLSearchParams({ + doctype: frm.doc.doctype, + name: frm.doc.name, + supplier: data.supplier, + print_format: data.print_format || "Standard", + language: data.language || frappe.boot.lang, + letter_head: data.letter_head || frm.doc.letter_head || "", + }).toString() + ) + ); + if (!w) { + frappe.msgprint(__("Please enable pop-ups")); + return; + } + }, + "Download PDF for Supplier", + "Download" + ); }, - 'Download PDF for Supplier', - 'Download'); - }, - __("Tools")); + __("Tools") + ); - frm.page.set_inner_btn_group_as_primary(__('Create')); + frm.page.set_inner_btn_group_as_primary(__("Create")); } - }, make_supplier_quotation: function(frm) { diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index bdbc9ce0b73..dbc36449570 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -389,10 +389,17 @@ def create_rfq_items(sq_doc, supplier, data): @frappe.whitelist() -def get_pdf(doctype, name, supplier): - doc = get_rfq_doc(doctype, name, supplier) - if doc: - download_pdf(doctype, name, doc=doc) +def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None): + # permissions get checked in `download_pdf` + if doc := get_rfq_doc(doctype, name, supplier): + download_pdf( + doctype, + name, + print_format, + doc=doc, + language=language, + letter_head=letter_head or None, + ) def get_rfq_doc(doctype, name, supplier): diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 7776ab8ec84..c5b369bedd5 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -40,12 +40,13 @@ "total", "net_total", "taxes_section", + "tax_category", "taxes_and_charges", "column_break_34", - "tax_category", - "column_break_36", "shipping_rule", + "column_break_36", "incoterm", + "named_place", "section_break_38", "taxes", "totals", @@ -830,6 +831,12 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-shopping-cart", @@ -837,7 +844,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-11-17 17:27:32.179686", + "modified": "2022-12-12 18:35:39.740974", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index bf077282bf8..d4972973d0b 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -347,16 +347,21 @@ class StatusUpdater(Document): ) def warn_about_bypassing_with_role(self, item, qty_or_amount, role): - action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling") + if qty_or_amount == "qty": + msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.") + else: + msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.") - msg = _("{0} of {1} {2} ignored for item {3} because you have {4} role.").format( - action, - _(item["target_ref_field"].title()), - frappe.bold(item["reduce_by"]), - frappe.bold(item.get("item_code")), - role, + frappe.msgprint( + msg.format( + _(item["target_ref_field"].title()), + frappe.bold(item["reduce_by"]), + frappe.bold(item.get("item_code")), + role, + ), + indicator="orange", + alert=True, ) - frappe.msgprint(msg, indicator="orange", alert=True) def update_qty(self, update_modified=True): """Updates qty or amount at row level diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index fe7b4e17f0b..c26b064c4c5 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -102,7 +102,7 @@ } ], "links": [], - "modified": "2021-06-30 13:09:14.228756", + "modified": "2022-12-15 11:11:02.131986", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", @@ -121,16 +121,6 @@ "share": 1, "write": 1 }, - { - "create": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Guest", - "share": 1 - }, { "create": 1, "delete": 1, @@ -170,5 +160,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 6e7ba1fd5bc..bd49bdc925c 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -6,7 +6,9 @@ from collections import Counter import frappe from frappe import _ +from frappe.desk.form.assign_to import add as add_assignment from frappe.model.document import Document +from frappe.share import add_docshare from frappe.utils import get_url, getdate, now from frappe.utils.verified_command import get_signed_params @@ -130,21 +132,18 @@ class Appointment(Document): self.party = lead.name def auto_assign(self): - from frappe.desk.form.assign_to import add as add_assignemnt - existing_assignee = self.get_assignee_from_latest_opportunity() if existing_assignee: # If the latest opportunity is assigned to someone # Assign the appointment to the same - add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]}) + self.assign_agent(existing_assignee) return if self._assign: return available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time)) for agent in available_agents: if _check_agent_availability(agent, self.scheduled_time): - agent = agent[0] - add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) + self.assign_agent(agent[0]) break def get_assignee_from_latest_opportunity(self): @@ -199,9 +198,15 @@ class Appointment(Document): params = {"email": self.customer_email, "appointment": self.name} return get_url(verify_route + "?" + get_signed_params(params)) + def assign_agent(self, agent): + if not frappe.has_permission(doc=self, user=agent): + add_docshare(self.doctype, self.name, agent, flags={"ignore_share_permission": True}) + + add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]}) + def _get_agents_sorted_by_asc_workload(date): - appointments = frappe.db.get_list("Appointment", fields="*") + appointments = frappe.get_all("Appointment", fields="*") agent_list = _get_agent_list_as_strings() if not appointments: return agent_list @@ -226,7 +231,7 @@ def _get_agent_list_as_strings(): def _check_agent_availability(agent_email, scheduled_time): - appointemnts_at_scheduled_time = frappe.get_list( + appointemnts_at_scheduled_time = frappe.get_all( "Appointment", filters={"scheduled_time": scheduled_time} ) for appointment in appointemnts_at_scheduled_time: diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 4b26e4901bd..436eb10c888 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-08-27 10:56:48.309824", "doctype": "DocType", "editable_grid": 1, @@ -101,7 +102,8 @@ } ], "issingle": 1, - "modified": "2019-11-26 12:14:17.669366", + "links": [], + "modified": "2022-12-15 11:10:13.517742", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", @@ -117,13 +119,6 @@ "share": 1, "write": 1 }, - { - "email": 1, - "print": 1, - "read": 1, - "role": "Guest", - "share": 1 - }, { "create": 1, "email": 1, @@ -147,5 +142,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6bc17a3675a..a2f87cb2ad8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -427,6 +427,7 @@ scheduler_events = { "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", + "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 36466ff6d7d..f568264c908 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1154,6 +1154,36 @@ class TestWorkOrder(FrappeTestCase): except frappe.MandatoryError: self.fail("Batch generation causing failing in Work Order") + @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1}) + def test_auto_serial_no_creation(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + fg_item = frappe.generate_hash(length=20) + child_item = frappe.generate_hash(length=20) + + bom_tree = {fg_item: {child_item: {}}} + + create_nested_bom(bom_tree, prefix="") + + item = frappe.get_doc("Item", fg_item) + item.has_serial_no = 1 + item.serial_no_series = f"{item.name}.#####" + item.save() + + try: + wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True) + serial_nos = wo_order.serial_no + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + stock_entry.set_work_order_details() + stock_entry.set_serial_no_batch_for_finished_good() + for row in stock_entry.items: + if row.item_code == fg_item: + self.assertTrue(row.serial_no) + self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos))) + + except frappe.MandatoryError: + self.fail("Batch generation causing failing in Work Order") + @change_settings( "Manufacturing Settings", {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 58d8de24993..aa57bc2168e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -298,7 +298,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } make_payment_request() { - var me = this; + let me = this; const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype)) ? "Inward" : "Outward"; @@ -314,7 +314,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if(!r.exc){ - var doc = frappe.model.sync(r.message); + frappe.model.sync(r.message); frappe.set_route("Form", r.message.doctype, r.message.name); } } diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 08918f4d61a..eb2c0a48ac5 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -43,12 +43,13 @@ "total", "net_total", "taxes_section", + "tax_category", "taxes_and_charges", "column_break_36", - "tax_category", - "column_break_34", "shipping_rule", + "column_break_34", "incoterm", + "named_place", "section_break_36", "taxes", "section_break_39", @@ -1059,13 +1060,19 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-11-17 17:20:54.984348", + "modified": "2022-12-12 18:32:28.671332", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 9ec32cbfc67..ccea8407ab8 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -58,12 +58,13 @@ "total", "net_total", "taxes_section", + "tax_category", "taxes_and_charges", "column_break_38", - "tax_category", - "column_break_49", "shipping_rule", + "column_break_49", "incoterm", + "named_place", "section_break_40", "taxes", "section_break_43", @@ -1630,13 +1631,19 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-11-17 17:22:00.413878", + "modified": "2022-12-12 18:34:00.681780", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index d6f23780942..07ee2890c46 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -70,9 +70,6 @@ class Company(NestedSet): self.abbr = self.abbr.strip() - # if self.get('__islocal') and len(self.abbr) > 5: - # frappe.throw(_("Abbreviation cannot have more than 5 characters")) - if not self.abbr.strip(): frappe.throw(_("Abbreviation is mandatory")) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index c18a4b2214b..4256a7d8312 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -204,7 +204,7 @@ class TransactionDeletionRecord(Document): @frappe.whitelist() def get_doctypes_to_be_ignored(): - doctypes_to_be_ignored_list = [ + doctypes_to_be_ignored = [ "Account", "Cost Center", "Warehouse", @@ -223,4 +223,7 @@ def get_doctypes_to_be_ignored(): "Customer", "Supplier", ] - return doctypes_to_be_ignored_list + + doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or []) + + return doctypes_to_be_ignored diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 80e4bcb640f..165a56b7839 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -57,12 +57,13 @@ "total", "net_total", "taxes_section", + "tax_category", "taxes_and_charges", "column_break_43", - "tax_category", - "column_break_39", "shipping_rule", + "column_break_39", "incoterm", + "named_place", "section_break_41", "taxes", "section_break_44", @@ -1388,13 +1389,19 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2022-11-17 17:22:42.860790", + "modified": "2022-12-12 18:38:53.067799", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 5de7fedc4e0..aff5e0539c7 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -192,13 +192,13 @@ class PickList(Document): if item_map.get(key): item_map[key].qty += item.qty - item_map[key].stock_qty += item.stock_qty + item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty")) else: item_map[key] = item # maintain count of each item (useful to limit get query) self.item_count_map.setdefault(item_code, 0) - self.item_count_map[item_code] += item.stock_qty + self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty")) return item_map.values() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index ab91d7c8c94..8f043585b89 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -58,12 +58,13 @@ "total", "net_total", "taxes_charges_section", + "tax_category", "taxes_and_charges", "shipping_col", - "tax_category", - "column_break_53", "shipping_rule", + "column_break_53", "incoterm", + "named_place", "taxes_section", "taxes", "totals", @@ -1225,13 +1226,19 @@ "fieldtype": "Link", "label": "Incoterm", "options": "Incoterm" + }, + { + "depends_on": "incoterm", + "fieldname": "named_place", + "fieldtype": "Data", + "label": "Named Place" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-11-17 17:29:30.067536", + "modified": "2022-12-12 18:40:32.447752", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index a2748d0d09a..541d4d17e18 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -766,13 +766,13 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note): @frappe.whitelist() def auto_fetch_serial_number( - qty: float, + qty: int, item_code: str, warehouse: str, posting_date: Optional[str] = None, batch_nos: Optional[Union[str, List[str]]] = None, for_doctype: Optional[str] = None, - exclude_sr_nos: Optional[List[str]] = None, + exclude_sr_nos=None, ) -> List[str]: filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f6c53f7bca0..0d8675a1f53 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4,12 +4,24 @@ import json from collections import defaultdict +from typing import Dict import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate +from frappe.utils import ( + add_days, + cint, + comma_or, + cstr, + flt, + format_time, + formatdate, + getdate, + nowdate, + today, +) import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -2239,16 +2251,16 @@ class StockEntry(StockController): d.qty -= process_loss_dict[d.item_code][1] def set_serial_no_batch_for_finished_good(self): - serial_nos = "" + serial_nos = [] if self.pro_doc.serial_no: - serial_nos = self.get_serial_nos_for_fg() + serial_nos = self.get_serial_nos_for_fg() or [] for row in self.items: if row.is_finished_item and row.item_code == self.pro_doc.production_item: if serial_nos: row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)]) - def get_serial_nos_for_fg(self, args): + def get_serial_nos_for_fg(self): fields = [ "`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`", @@ -2264,9 +2276,7 @@ class StockEntry(StockController): ] stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) - - if self.pro_doc.serial_no: - return self.get_available_serial_nos(stock_entries) + return self.get_available_serial_nos(stock_entries) def get_available_serial_nos(self, stock_entries): used_serial_nos = [] @@ -2705,3 +2715,62 @@ def get_stock_entry_data(work_order): ) .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) ).run(as_dict=1) + + +def audit_incorrect_valuation_entries(): + # Audit of stock transfer entries having incorrect valuation + from erpnext.controllers.stock_controller import create_repost_item_valuation_entry + + stock_entries = get_incorrect_stock_entries() + + for stock_entry, values in stock_entries.items(): + reposting_data = frappe._dict( + { + "posting_date": values.posting_date, + "posting_time": values.posting_time, + "voucher_type": "Stock Entry", + "voucher_no": stock_entry, + "company": values.company, + } + ) + + create_repost_item_valuation_entry(reposting_data) + + +def get_incorrect_stock_entries() -> Dict: + stock_entry = frappe.qb.DocType("Stock Entry") + stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") + transfer_purposes = [ + "Material Transfer", + "Material Transfer for Manufacture", + "Send to Subcontractor", + ] + + query = ( + frappe.qb.from_(stock_entry) + .inner_join(stock_ledger_entry) + .on(stock_entry.name == stock_ledger_entry.voucher_no) + .select( + stock_entry.name, + stock_entry.company, + stock_entry.posting_date, + stock_entry.posting_time, + Sum(stock_ledger_entry.stock_value_difference).as_("stock_value"), + ) + .where( + (stock_entry.docstatus == 1) + & (stock_entry.purpose.isin(transfer_purposes)) + & (stock_ledger_entry.modified > add_days(today(), -2)) + ) + .groupby(stock_ledger_entry.voucher_detail_no) + .having(Sum(stock_ledger_entry.stock_value_difference) != 0) + ) + + data = query.run(as_dict=True) + stock_entries = {} + + for row in data: + if abs(row.stock_value) > 0.1 and row.name not in stock_entries: + stock_entries.setdefault(row.name, row) + + return stock_entries diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index b574b718fe1..680d209735e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,7 +5,7 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate, nowtime, today +from frappe.utils import add_days, flt, now, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -17,6 +17,8 @@ from erpnext.stock.doctype.item.test_item import ( from erpnext.stock.doctype.serial_no.serial_no import * # noqa from erpnext.stock.doctype.stock_entry.stock_entry import ( FinishedGoodError, + audit_incorrect_valuation_entries, + get_incorrect_stock_entries, move_sample_to_retention_warehouse, ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -1614,6 +1616,44 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(BatchExpiredError, se.save) + def test_audit_incorrect_stock_entries(self): + item_code = "Test Incorrect Valuation Rate Item - 001" + create_item(item_code=item_code, is_stock_item=1) + + make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + posting_date=add_days(nowdate(), -10), + qty=2, + rate=500, + to_warehouse="_Test Warehouse - _TC", + ) + + transfer_entry = make_stock_entry( + item_code=item_code, + purpose="Material Transfer", + qty=2, + rate=500, + from_warehouse="_Test Warehouse - _TC", + to_warehouse="_Test Warehouse 1 - _TC", + ) + + sle_name = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": transfer_entry.name, "actual_qty": (">", 0)}, "name" + ) + + frappe.db.set_value( + "Stock Ledger Entry", sle_name, {"modified": add_days(now(), -1), "stock_value_difference": 10} + ) + + stock_entries = get_incorrect_stock_entries() + self.assertTrue(transfer_entry.name in stock_entries) + + audit_incorrect_valuation_entries() + + stock_entries = get_incorrect_stock_entries() + self.assertFalse(transfer_entry.name in stock_entries) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3a0b38a0fcd..398b3c98e38 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -715,8 +715,8 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): def get_stock_balance_for( item_code: str, warehouse: str, - posting_date: str, - posting_time: str, + posting_date, + posting_time, batch_no: Optional[str] = None, with_valuation_rate: bool = True, ): diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 31dccf6944d..1741d654601 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -828,9 +828,9 @@ def insert_item_price(args): ): if frappe.has_permission("Item Price", "write"): price_list_rate = ( - (args.rate + args.discount_amount) / args.get("conversion_factor") + (flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor") if args.get("conversion_factor") - else (args.rate + args.discount_amount) + else (flt(args.rate) + flt(args.discount_amount)) ) item_price = frappe.db.get_value( diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index a6fc049cbde..c4358b809fc 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -82,7 +82,7 @@ def get_item_info(filters): item.safety_stock, item.lead_time_days, ) - .where(item.is_stock_item == 1) + .where((item.is_stock_item == 1) & (item.disabled == 0)) ) if brand := filters.get("brand"): diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index a401983d2ad..4cba02a6acc 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1849,6 +1849,8 @@ Outstanding Amt,Offener Betrag, Outstanding Cheques and Deposits to clear,Ausstehende Schecks und Anzahlungen zum verbuchen, Outstanding for {0} cannot be less than zero ({1}),Ausstände für {0} können nicht kleiner als Null sein ({1}), Outward taxable supplies(zero rated),Steuerpflichtige Lieferungen aus dem Ausland (null bewertet), +Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Annahme bzw. Lieferung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben." +Overbilling of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Abrechnung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben." Overdue,Überfällig, Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1}, Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:, @@ -9916,4 +9918,3 @@ Cost and Freight,Kosten und Fracht, Delivered at Place,Geliefert benannter Ort, Delivered at Place Unloaded,Geliefert benannter Ort entladen, Delivered Duty Paid,Geliefert verzollt, -{0} of {1} {2} ignored for item {3} because you have {4} role,"{0} von Artikel {3} mit {1} {2} wurde ignoriert, weil Sie die Rolle {4} haben." diff --git a/erpnext/www/book-appointment/__init__.py b/erpnext/www/book-appointment/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/www/book-appointment/verify/__init__.py b/erpnext/www/book-appointment/verify/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index 5562cbd4710..9750e4d850d 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -2,8 +2,6 @@ frappe.ready(async () => { initialise_select_date(); }) -window.holiday_list = []; - async function initialise_select_date() { navigate_to_page(1); await get_global_variables(); @@ -20,7 +18,6 @@ async function get_global_variables() { window.timezones = (await frappe.call({ method:'erpnext.www.book_appointment.index.get_timezones' })).message; - window.holiday_list = window.appointment_settings.holiday_list; } function setup_timezone_selector() { diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 06e99da3f94..dfca9465ed1 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -26,8 +26,12 @@ def get_context(context): @frappe.whitelist(allow_guest=True) def get_appointment_settings(): - settings = frappe.get_doc("Appointment Booking Settings") - settings.holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) + settings = frappe.get_cached_value( + "Appointment Booking Settings", + None, + ["advance_booking_days", "appointment_duration", "success_redirect_url"], + as_dict=True, + ) return settings @@ -106,7 +110,7 @@ def create_appointment(date, time, tz, contact): appointment.customer_details = contact.get("notes", None) appointment.customer_email = contact.get("email", None) appointment.status = "Open" - appointment.insert() + appointment.insert(ignore_permissions=True) return appointment diff --git a/erpnext/www/book_appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py index 1a5ba9de7e5..3beb8667ae7 100644 --- a/erpnext/www/book_appointment/verify/index.py +++ b/erpnext/www/book_appointment/verify/index.py @@ -2,7 +2,6 @@ import frappe from frappe.utils.verified_command import verify_request -@frappe.whitelist(allow_guest=True) def get_context(context): if not verify_request(): context.success = False