diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index bbbbe0b6f9d..5da1c0d35d9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -165,6 +165,10 @@ frappe.ui.form.on('Payment Entry', { filters: { reference_doctype: row.reference_doctype, reference_name: row.reference_name, + company: doc.company, + status: ["!=", "Paid"], + outstanding_amount: [">", 0], // for compatibility with old data + docstatus: 1, }, }; }); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1fda91185a7..5224db65f03 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2605,6 +2605,7 @@ def get_open_payment_requests_for_references(references=None): .where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs))) .where(PR.status != "Paid") .where(PR.docstatus == 1) + .where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount .orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc) ).run(as_dict=True) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index d949f7857c1..68055546c24 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -848,12 +848,7 @@ def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, open_payment_requests = frappe.get_list( "Payment Request", - filters={ - **filters, - "status": ["!=", "Paid"], - "outstanding_amount": ["!=", 0], # for compatibility with old data - "docstatus": 1, - }, + filters=filters, fields=["name", "grand_total", "outstanding_amount"], order_by="transaction_date ASC,creation ASC", ) diff --git a/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py b/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py new file mode 100644 index 00000000000..02ad5684792 --- /dev/null +++ b/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py @@ -0,0 +1,14 @@ +from frappe import _ + + +def get_data(): + return { + "fieldname": "payment_request", + "internal_links": { + "Payment Entry": ["references", "payment_request"], + "Payment Order": ["references", "payment_order"], + }, + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]}, + ], + } diff --git a/erpnext/accounts/doctype/payment_request/payment_request_list.js b/erpnext/accounts/doctype/payment_request/payment_request_list.js index 183ca7c4584..1027385aaaf 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request_list.js +++ b/erpnext/accounts/doctype/payment_request/payment_request_list.js @@ -1,19 +1,18 @@ +const INDICATORS = { + "Partially Paid": "orange", + Cancelled: "red", + Draft: "gray", + Failed: "red", + Initiated: "green", + Paid: "blue", + Requested: "green", +}; + frappe.listview_settings["Payment Request"] = { add_fields: ["status"], get_indicator: function (doc) { - if (doc.status == "Draft") { - return [__("Draft"), "gray", "status,=,Draft"]; - } - if (doc.status == "Requested") { - return [__("Requested"), "green", "status,=,Requested"]; - } else if (doc.status == "Initiated") { - return [__("Initiated"), "green", "status,=,Initiated"]; - } else if (doc.status == "Partially Paid") { - return [__("Partially Paid"), "orange", "status,=,Partially Paid"]; - } else if (doc.status == "Paid") { - return [__("Paid"), "blue", "status,=,Paid"]; - } else if (doc.status == "Cancelled") { - return [__("Cancelled"), "red", "status,=,Cancelled"]; - } + if (!doc.status || !INDICATORS[doc.status]) return; + + return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`]; }, }; diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 854523f1009..f5a9d17f088 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -48,6 +48,7 @@ "shipping_address", "company_address", "company_address_display", + "company_contact_person", "currency_and_price_list", "currency", "conversion_rate", @@ -1557,12 +1558,19 @@ "fieldname": "update_billed_amount_in_delivery_note", "fieldtype": "Check", "label": "Update Billed Amount in Delivery Note" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:00:34.268756", + "modified": "2024-11-26 13:10:50.309570", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 950a7a3eb29..2855d999233 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1643,6 +1643,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + # Cost of Item is zero in Purchase Receipt + pr = make_purchase_receipt(qty=1, rate=0) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 0) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 150 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 150) + # Increase the cost of the item pr = make_purchase_receipt(qty=1, rate=100) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 632392511bd..b58c7750d78 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -160,8 +160,9 @@ "dispatch_address", "company_address_section", "company_address", - "company_addr_col_break", "company_address_display", + "company_addr_col_break", + "company_contact_person", "terms_tab", "payment_schedule_section", "ignore_default_payment_terms_template", @@ -2171,6 +2172,13 @@ "label": "Update Outstanding for Self", "no_copy": 1, "print_hide": 1 + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", @@ -2183,7 +2191,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-07-18 15:30:39.428519", + "modified": "2024-11-26 12:34:09.110690", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ff81a114d54..b3ef8e23985 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1556,8 +1556,12 @@ class SalesInvoice(SellingController): ) def update_project(self): - if self.project: - project = frappe.get_doc("Project", self.project) + unique_projects = list(set([d.project for d in self.get("items") if d.project])) + if self.project and self.project not in unique_projects: + unique_projects.append(self.project) + + for p in unique_projects: + project = frappe.get_doc("Project", p) project.update_billed_amount() project.db_update() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9b303430455..6e4c6002016 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3760,6 +3760,102 @@ class TestSalesInvoice(FrappeTestCase): self.assertTrue(jv) self.assertEqual(jv[0], si.grand_total) + @change_settings("Accounts Settings", {"enable_common_party_accounting": True}) + def test_common_party_with_different_currency_in_debtor_and_creditor(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + from erpnext.setup.utils import get_exchange_rate + + creditors = create_account( + account_name="Creditors INR", + parent_account="Accounts Payable - _TC", + company="_Test Company", + account_currency="INR", + account_type="Payable", + ) + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Common Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Party INR").name + supp_doc = frappe.get_doc("Supplier", supplier) + supp_doc.default_currency = "INR" + test_account_details = { + "company": "_Test Company", + "account": creditors, + } + supp_doc.append("accounts", test_account_details) + supp_doc.save() + + # create a party link between customer & supplier + create_party_link("Supplier", supplier, customer) + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=get_exchange_rate("USD", "INR"), + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, "Paid") + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + { + "account": si.debit_to, + "party_type": "Customer", + "party": si.customer, + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="credit_in_account_currency", + ) + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + def test_total_billed_amount(self): + si = create_sales_invoice(do_not_submit=True) + + project = frappe.new_doc("Project") + project.project_name = "Test Total Billed Amount" + project.save() + + si.project = project.name + si.save() + si.submit() + + doc = frappe.get_doc("Project", project.name) + self.assertEqual(doc.total_billed_amount, si.grand_total) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index a19eedd8b72..ceb3a7f784f 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -29,6 +29,12 @@ from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.utilities.regional import temporary_flag +try: + from frappe.contacts.doctype.address.address import render_address as _render_address +except ImportError: + # Older frappe versions where this function is not available + from frappe.contacts.doctype.address.address import get_address_display as _render_address + PURCHASE_TRANSACTION_TYPES = { "Supplier Quotation", "Purchase Order", @@ -985,10 +991,4 @@ def add_party_account(party_type, party, company, account): def render_address(address, check_permissions=True): - try: - from frappe.contacts.doctype.address.address import render_address as _render - except ImportError: - # Older frappe versions where this function is not available - from frappe.contacts.doctype.address.address import get_address_display as _render - - return frappe.call(_render, address, check_permissions=check_permissions) + return frappe.call(_render_address, address, check_permissions=check_permissions) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a5c90a37d4d..7868b06fe33 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1004,7 +1004,7 @@ class ReceivablePayableReport: def get_columns(self): self.columns = [] - self.add_column(_("Posting Date"), fieldtype="Date") + self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date") self.add_column( label=_("Party Type"), fieldname="party_type", @@ -1018,8 +1018,15 @@ class ReceivablePayableReport: options="party_type", width=180, ) + if self.account_type == "Receivable": + label = _("Receivable Account") + elif self.account_type == "Payable": + label = _("Payable Account") + else: + label = _("Party Account") + self.add_column( - label=self.account_type + " Account", + label=label, fieldname="party_account", fieldtype="Link", options="Account", @@ -1057,7 +1064,7 @@ class ReceivablePayableReport: width=180, ) - self.add_column(label=_("Due Date"), fieldtype="Date") + self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date") if self.account_type == "Payable": self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index baee062cf84..50f6f3b03c5 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -7,6 +7,7 @@ from frappe import _ from frappe.utils import cint, flt from erpnext.accounts.report.financial_statements import ( + compute_growth_view_data, get_columns, get_data, get_filtered_list_for_consolidated_report, @@ -101,6 +102,9 @@ def execute(filters=None): period_list, asset, liability, equity, provisional_profit_loss, currency, filters ) + if filters.get("selected_view") == "Growth": + compute_growth_view_data(data, period_list) + return columns, data, message, chart, report_summary, primitive_summary diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 56f3bd1caa4..16886cd939a 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt +import copy import functools import math import re @@ -653,3 +654,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list): filtered_summary_list.append(period) return filtered_summary_list + + +def compute_growth_view_data(data, columns): + data_copy = copy.deepcopy(data) + + for row_idx in range(len(data_copy)): + for column_idx in range(1, len(columns)): + previous_period_key = columns[column_idx - 1].get("key") + current_period_key = columns[column_idx].get("key") + current_period_value = data_copy[row_idx].get(current_period_key) + previous_period_value = data_copy[row_idx].get(previous_period_key) + annual_growth = 0 + + if current_period_value is None: + data[row_idx][current_period_key] = None + continue + + if previous_period_value == 0 and current_period_value > 0: + annual_growth = 1 + + elif previous_period_value > 0: + annual_growth = (current_period_value - previous_period_value) / previous_period_value + + growth_percent = round(annual_growth * 100, 2) + + data[row_idx][current_period_key] = growth_percent + + +def compute_margin_view_data(data, columns, accumulated_values): + if not columns: + return + + if not accumulated_values: + columns.append({"key": "total"}) + + data_copy = copy.deepcopy(data) + + base_row = None + for row in data_copy: + if row.get("account_name") == _("Income"): + base_row = row + break + + if not base_row: + return + + for row_idx in range(len(data_copy)): + # Taking the total income from each column (for all the financial years) as the base (100%) + row = data_copy[row_idx] + if not row: + continue + + for column in columns: + curr_period = column.get("key") + base_value = base_row[curr_period] + curr_value = row[curr_period] + + if curr_value is None or base_value <= 0: + data[row_idx][curr_period] = None + continue + + margin_percent = round((curr_value / base_value) * 100, 2) + + data[row_idx][curr_period] = margin_percent diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index edc0ecbd581..833bb78a329 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -402,10 +402,10 @@ class GrossProfitGenerator: self.load_invoice_items() self.get_delivery_notes() + self.load_product_bundle() if filters.group_by == "Invoice": self.group_items_by_invoice() - self.load_product_bundle() self.load_non_stock_items() self.get_returned_invoice_items() self.process() @@ -617,6 +617,7 @@ class GrossProfitGenerator: if packed_item.get("parent_detail_docname") == row.item_row: packed_item_row = row.copy() packed_item_row.warehouse = packed_item.warehouse + packed_item_row.qty = packed_item.total_qty * -1 buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code) return flt(buying_amount, self.currency_precision) @@ -649,7 +650,9 @@ class GrossProfitGenerator: else: my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) if (row.update_stock or row.dn_detail) and my_sle: - parenttype, parent = row.parenttype, row.parent + parenttype = row.parenttype + parent = row.invoice or row.parent + if row.dn_detail: parenttype, parent = "Delivery Note", row.delivery_note @@ -797,6 +800,7 @@ class GrossProfitGenerator: `tabSales Invoice`.project, `tabSales Invoice`.update_stock, `tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, + `tabSales Invoice`.base_net_total as "invoice_base_net_total", `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, `tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail, @@ -857,6 +861,7 @@ class GrossProfitGenerator: """ grouped = OrderedDict() + product_bundles = self.product_bundles.get("Sales Invoice", {}) for row in self.si_list: # initialize list with a header row for each new parent @@ -867,8 +872,7 @@ class GrossProfitGenerator: ) # if item is a bundle, add it's components as seperate rows - if frappe.db.exists("Product Bundle", row.item_code): - bundled_items = self.get_bundle_items(row) + if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code): for x in bundled_items: bundle_item = self.get_bundle_item_row(row, x) grouped.get(row.parent).append(bundle_item) @@ -904,47 +908,40 @@ class GrossProfitGenerator: "item_row": None, "is_return": row.is_return, "cost_center": row.cost_center, - "base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"), + "base_net_amount": row.invoice_base_net_total, } ) - def get_bundle_items(self, product_bundle): - return frappe.get_all( - "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] - ) - - def get_bundle_item_row(self, product_bundle, item): - item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code) - + def get_bundle_item_row(self, row, item): return frappe._dict( { - "parent_invoice": product_bundle.item_code, - "indent": product_bundle.indent + 1, + "parent_invoice": row.item_code, + "parenttype": row.parenttype, + "indent": row.indent + 1, "parent": None, "invoice_or_item": item.item_code, - "posting_date": product_bundle.posting_date, - "posting_time": product_bundle.posting_time, - "project": product_bundle.project, - "customer": product_bundle.customer, - "customer_group": product_bundle.customer_group, + "posting_date": row.posting_date, + "posting_time": row.posting_time, + "project": row.project, + "customer": row.customer, + "customer_group": row.customer_group, "item_code": item.item_code, - "item_name": item_name, - "description": description, - "warehouse": product_bundle.warehouse, - "item_group": item_group, - "brand": brand, - "dn_detail": product_bundle.dn_detail, - "delivery_note": product_bundle.delivery_note, - "qty": (flt(product_bundle.qty) * flt(item.qty)), - "item_row": None, - "is_return": product_bundle.is_return, - "cost_center": product_bundle.cost_center, + "item_name": item.item_name, + "description": item.description, + "warehouse": item.warehouse or row.warehouse, + "update_stock": row.update_stock, + "item_group": "", + "brand": "", + "dn_detail": row.dn_detail, + "delivery_note": row.delivery_note, + "qty": item.total_qty * -1, + "item_row": row.item_row, + "is_return": row.is_return, + "cost_center": row.cost_center, + "invoice": row.parent, } ) - def get_bundle_item_details(self, item_code): - return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"]) - def get_stock_ledger_entries(self, item_code, warehouse): if item_code and warehouse: if (item_code, warehouse) not in self.sle: diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index da50668f630..bcdda154202 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -7,6 +7,8 @@ from frappe import _ from frappe.utils import flt from erpnext.accounts.report.financial_statements import ( + compute_growth_view_data, + compute_margin_view_data, get_columns, get_data, get_filtered_list_for_consolidated_report, @@ -68,6 +70,12 @@ def execute(filters=None): period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters ) + if filters.get("selected_view") == "Growth": + compute_growth_view_data(data, period_list) + + if filters.get("selected_view") == "Margin": + compute_margin_view_data(data, period_list, filters.accumulated_values) + return columns, data, None, chart, report_summary, primitive_summary diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e7d8bc9ddd4..6a5914ddbe0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2313,6 +2313,12 @@ class AccountsController(TransactionBase): secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) primary_account_currency = get_account_currency(primary_account) secondary_account_currency = get_account_currency(secondary_account) + default_currency = erpnext.get_company_currency(self.company) + + # Determine if multi-currency journal entry is needed + multi_currency = ( + primary_account_currency != default_currency or secondary_account_currency != default_currency + ) jv = frappe.new_doc("Journal Entry") jv.voucher_type = "Journal Entry" @@ -2337,7 +2343,7 @@ class AccountsController(TransactionBase): advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company) advance_entry.is_advance = "Yes" - # update dimesions + # Update dimensions dimensions_dict = frappe._dict() active_dimensions = get_dimensions()[0] for dim in active_dimensions: @@ -2346,17 +2352,58 @@ class AccountsController(TransactionBase): reconcilation_entry.update(dimensions_dict) advance_entry.update(dimensions_dict) - if self.doctype == "Sales Invoice": - reconcilation_entry.credit_in_account_currency = self.outstanding_amount - advance_entry.debit_in_account_currency = self.outstanding_amount + # Calculate exchange rates if necessary + if multi_currency: + # Exchange rates for primary and secondary accounts + exc_rate_primary_to_default = ( + 1 + if primary_account_currency == default_currency + else get_exchange_rate(primary_account_currency, default_currency, self.posting_date) + ) + exc_rate_secondary_to_default = ( + 1 + if secondary_account_currency == default_currency + else get_exchange_rate(secondary_account_currency, default_currency, self.posting_date) + ) + exc_rate_secondary_to_primary = ( + 1 + if secondary_account_currency == primary_account_currency + else get_exchange_rate( + secondary_account_currency, primary_account_currency, self.posting_date + ) + ) + + # Convert outstanding amount from secondary to primary account currency, if needed + + os_in_default_currency = self.outstanding_amount * exc_rate_secondary_to_default + os_in_primary_currency = self.outstanding_amount * exc_rate_secondary_to_primary + + if self.doctype == "Sales Invoice": + # Calculate credit and debit values for reconciliation and advance entries + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.credit = os_in_default_currency + + advance_entry.debit_in_account_currency = os_in_primary_currency + advance_entry.debit = os_in_default_currency + else: + advance_entry.credit_in_account_currency = os_in_primary_currency + advance_entry.credit = os_in_default_currency + + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit = os_in_default_currency + + # Set exchange rates for entries + reconcilation_entry.exchange_rate = exc_rate_secondary_to_default + advance_entry.exchange_rate = exc_rate_primary_to_default else: - advance_entry.credit_in_account_currency = self.outstanding_amount - reconcilation_entry.debit_in_account_currency = self.outstanding_amount - - default_currency = erpnext.get_company_currency(self.company) - if primary_account_currency != default_currency or secondary_account_currency != default_currency: - jv.multi_currency = 1 + if self.doctype == "Sales Invoice": + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + advance_entry.debit_in_account_currency = self.outstanding_amount + else: + advance_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + jv.multi_currency = multi_currency jv.append("accounts", reconcilation_entry) jv.append("accounts", advance_entry) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index cba38e46b8c..1cd98508e0b 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -68,19 +68,13 @@ class SellingController(StockController): if customer: from erpnext.accounts.party import _get_party_details - fetch_payment_terms_template = False - if self.get("__islocal") or self.company != frappe.db.get_value( - self.doctype, self.name, "company" - ): - fetch_payment_terms_template = True - party_details = _get_party_details( customer, ignore_permissions=self.flags.ignore_permissions, doctype=self.doctype, company=self.company, posting_date=self.get("posting_date"), - fetch_payment_terms_template=fetch_payment_terms_template, + fetch_payment_terms_template=self.has_value_changed("company"), party_address=self.customer_address, shipping_address=self.shipping_address_name, company_address=self.get("company_address"), @@ -365,12 +359,32 @@ class SellingController(StockController): return il def has_product_bundle(self, item_code): - product_bundle = frappe.qb.DocType("Product Bundle") - return ( - frappe.qb.from_(product_bundle) - .select(product_bundle.name) - .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) - ).run() + product_bundle_items = getattr(self, "_product_bundle_items", None) + if product_bundle_items is None: + self._product_bundle_items = product_bundle_items = {} + + if item_code not in product_bundle_items: + self._fetch_product_bundle_items(item_code) + + return product_bundle_items[item_code] + + def _fetch_product_bundle_items(self, item_code): + product_bundle_items = self._product_bundle_items + items_to_fetch = {row.item_code for row in self.items if row.item_code not in product_bundle_items} + # fetch for requisite item_code even if it is not in items + items_to_fetch.add(item_code) + + items_with_product_bundle = { + row.new_item_code + for row in frappe.get_all( + "Product Bundle", + filters={"new_item_code": ("in", items_to_fetch), "disabled": 0}, + fields="new_item_code", + ) + } + + for item_code in items_to_fetch: + product_bundle_items[item_code] = item_code in items_with_product_bundle def get_already_delivered_qty(self, current_docname, so, so_detail): delivered_via_dn = frappe.db.sql( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 6bb03310389..5971587da56 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -89,10 +89,18 @@ class WorkOrder(Document): self.status = self.get_status() self.validate_workstation_type() + if self.source_warehouse: + self.set_warehouses() + validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) self.set_required_items(reset_only_qty=len(self.get("required_items"))) + def set_warehouses(self): + for row in self.required_items: + if not row.source_warehouse: + row.source_warehouse = self.source_warehouse + def validate_workstation_type(self): for row in self.operations: if not row.workstation and not row.workstation_type: diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7e2d5e63ec3..9f8aa84f836 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -421,7 +421,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe setup_sms() { var me = this; let blacklist = ['Purchase Invoice', 'BOM']; - if(this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status) + if(frappe.boot.sms_gateway_enabled && this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status) && !blacklist.includes(this.frm.doctype)) { this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); }); } @@ -990,7 +990,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe apply_discount_on_item(doc, cdt, cdn, field) { var item = frappe.get_doc(cdt, cdn); - if(!item.price_list_rate) { + if(item && !item.price_list_rate) { item[field] = 0.0; } else { this.price_list_rate(doc, cdt, cdn); diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 25620de834d..a23a841dbfa 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -9,40 +9,29 @@ erpnext.financial_statements = { data && column.colIndex >= 3 ) { - //Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns. - const lastAnnualValue = row[column.colIndex - 1].content; - const currentAnnualvalue = data[column.fieldname]; - if (currentAnnualvalue == undefined) return "NA"; //making this not applicable for undefined/null values - let annualGrowth = 0; - if (lastAnnualValue == 0 && currentAnnualvalue > 0) { - //If the previous year value is 0 and the current value is greater than 0 - annualGrowth = 1; - } else if (lastAnnualValue > 0) { - annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue; - } + const growthPercent = data[column.fieldname]; - const growthPercent = Math.round(annualGrowth * 10000) / 100; //calculating the rounded off percentage + if (growthPercent == undefined) return "NA"; //making this not applicable for undefined/null values - value = $(`${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}`); - if (growthPercent < 0) { - value = $(value).addClass("text-danger"); + if (column.fieldname === "total") { + value = $(`${growthPercent}`); } else { - value = $(value).addClass("text-success"); + value = $(`${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}`); + + if (growthPercent < 0) { + value = $(value).addClass("text-danger"); + } else { + value = $(value).addClass("text-success"); + } } value = $(value).wrap("
").parent().html(); return value; } else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) { - if (column.fieldname == "account" && data.account_name == __("Income")) { - //Taking the total income from each column (for all the financial years) as the base (100%) - this.baseData = row; - } if (column.colIndex >= 2) { - //Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns. - const currentAnnualvalue = data[column.fieldname]; - const baseValue = this.baseData[column.colIndex].content; - if (currentAnnualvalue == undefined || baseValue <= 0) return "NA"; - const marginPercent = Math.round((currentAnnualvalue / baseValue) * 10000) / 100; + const marginPercent = data[column.fieldname]; + + if (marginPercent == undefined) return "NA"; //making this not applicable for undefined/null values value = $(`${marginPercent + "%"}`); if (marginPercent < 0) value = $(value).addClass("text-danger"); diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 4bdd463f047..f014e6303ff 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -64,6 +64,17 @@ $.extend(erpnext.queries, { } }, + company_contact_query: function (doc) { + if (!doc.company) { + frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))])); + } + + return { + query: "frappe.contacts.doctype.contact.contact.contact_query", + filters: { link_doctype: "Company", link_name: doc.company }, + }; + }, + address_query: function (doc) { if (frappe.dynamic_link) { if (!doc[frappe.dynamic_link.fieldname]) { diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 40fa986951e..ee37c7843d3 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -95,8 +95,9 @@ "shipping_address", "company_address_section", "company_address", - "column_break_87", "company_address_display", + "column_break_87", + "company_contact_person", "terms_tab", "payment_schedule_section", "payment_terms_template", @@ -1066,13 +1067,20 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:04:21.567847", + "modified": "2024-11-26 12:43:29.293637", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index bd23440aaf0..48ff2756253 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -455,7 +455,9 @@ def _make_customer(source_name, ignore_permissions=False): raise except frappe.MandatoryError as e: mandatory_fields = e.args[0].split(":")[1].split(",") - mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] + mandatory_fields = [ + _(customer.meta.get_label(field.strip())) for field in mandatory_fields + ] frappe.local.message_log = [] lead_link = frappe.utils.get_link_to_form("Lead", lead_name) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 43e8e41f7f3..d4ab0978584 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -112,8 +112,9 @@ "dispatch_address", "col_break46", "company_address", - "column_break_92", "company_address_display", + "column_break_92", + "company_contact_person", "payment_schedule_section", "payment_terms_section", "payment_terms_template", @@ -1626,13 +1627,20 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2024-05-23 16:35:54.905804", + "modified": "2024-11-26 12:42:06.872527", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1711,4 +1719,4 @@ "title_field": "customer_name", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 0293f37d0ad..d56e0aa9829 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -41,6 +41,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran me.frm.set_query('shipping_address_name', erpnext.queries.address_query); me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query); me.frm.set_query('company_address', erpnext.queries.company_address_query); + me.frm.set_query('company_contact_person', erpnext.queries.company_contact_query); erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 60e69599dae..36e83f377a3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -109,8 +109,9 @@ "dispatch_address", "company_address_section", "company_address", - "column_break_101", "company_address_display", + "column_break_101", + "company_contact_person", "terms_tab", "tc_name", "terms", @@ -1395,13 +1396,20 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:05:02.854990", + "modified": "2024-11-26 12:44:28.258215", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index faf305cfe9c..b293a45580a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -907,7 +907,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate if adjust_incoming_rate: adjusted_amt = 0.0 - if item.billed_amt and item.amount: + if item.billed_amt is not None and item.amount is not None: adjusted_amt = flt(item.billed_amt) - flt(item.amount) item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 3b7812f96c2..fe0d1f84ad3 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -194,11 +194,11 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, str): qty_fields = [qty_fields] - distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children())) - integer_uoms = list( - filter( - lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None, - distinct_uoms, + distinct_uoms = tuple(set(uom for uom in (d.get(uom_field) for d in doc.get_all_children()) if uom)) + integer_uoms = set( + d[0] + for d in frappe.db.get_values( + "UOM", (("name", "in", distinct_uoms), ("must_be_whole_number", "=", 1)), cache=True ) )