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

chore: release v14
This commit is contained in:
ruthra kumar
2024-12-04 10:04:44 +05:30
committed by GitHub
29 changed files with 463 additions and 133 deletions

View File

@@ -165,6 +165,10 @@ frappe.ui.form.on('Payment Entry', {
filters: { filters: {
reference_doctype: row.reference_doctype, reference_doctype: row.reference_doctype,
reference_name: row.reference_name, reference_name: row.reference_name,
company: doc.company,
status: ["!=", "Paid"],
outstanding_amount: [">", 0], // for compatibility with old data
docstatus: 1,
}, },
}; };
}); });

View File

@@ -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(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
.where(PR.status != "Paid") .where(PR.status != "Paid")
.where(PR.docstatus == 1) .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) .orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
).run(as_dict=True) ).run(as_dict=True)

View File

@@ -848,12 +848,7 @@ def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len,
open_payment_requests = frappe.get_list( open_payment_requests = frappe.get_list(
"Payment Request", "Payment Request",
filters={ filters=filters,
**filters,
"status": ["!=", "Paid"],
"outstanding_amount": ["!=", 0], # for compatibility with old data
"docstatus": 1,
},
fields=["name", "grand_total", "outstanding_amount"], fields=["name", "grand_total", "outstanding_amount"],
order_by="transaction_date ASC,creation ASC", order_by="transaction_date ASC,creation ASC",
) )

View File

@@ -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"]},
],
}

View File

@@ -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"] = { frappe.listview_settings["Payment Request"] = {
add_fields: ["status"], add_fields: ["status"],
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status == "Draft") { if (!doc.status || !INDICATORS[doc.status]) return;
return [__("Draft"), "gray", "status,=,Draft"];
} return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
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"];
}
}, },
}; };

View File

@@ -48,6 +48,7 @@
"shipping_address", "shipping_address",
"company_address", "company_address",
"company_address_display", "company_address_display",
"company_contact_person",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@@ -1557,12 +1558,19 @@
"fieldname": "update_billed_amount_in_delivery_note", "fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note" "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", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-20 16:00:34.268756", "modified": "2024-11-26 13:10:50.309570",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -1643,6 +1643,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) 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 # Increase the cost of the item
pr = make_purchase_receipt(qty=1, rate=100) pr = make_purchase_receipt(qty=1, rate=100)

View File

@@ -160,8 +160,9 @@
"dispatch_address", "dispatch_address",
"company_address_section", "company_address_section",
"company_address", "company_address",
"company_addr_col_break",
"company_address_display", "company_address_display",
"company_addr_col_break",
"company_contact_person",
"terms_tab", "terms_tab",
"payment_schedule_section", "payment_schedule_section",
"ignore_default_payment_terms_template", "ignore_default_payment_terms_template",
@@ -2171,6 +2172,13 @@
"label": "Update Outstanding for Self", "label": "Update Outstanding for Self",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "company_contact_person",
"fieldtype": "Link",
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@@ -2183,7 +2191,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2024-07-18 15:30:39.428519", "modified": "2024-11-26 12:34:09.110690",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -1556,8 +1556,12 @@ class SalesInvoice(SellingController):
) )
def update_project(self): def update_project(self):
if self.project: unique_projects = list(set([d.project for d in self.get("items") if d.project]))
project = frappe.get_doc("Project", self.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.update_billed_amount()
project.db_update() project.db_update()

View File

@@ -3760,6 +3760,102 @@ class TestSalesInvoice(FrappeTestCase):
self.assertTrue(jv) self.assertTrue(jv)
self.assertEqual(jv[0], si.grand_total) 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): def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql( gl_entries = frappe.db.sql(

View File

@@ -29,6 +29,12 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
from erpnext.utilities.regional import temporary_flag from erpnext.utilities.regional import temporary_flag
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 = { PURCHASE_TRANSACTION_TYPES = {
"Supplier Quotation", "Supplier Quotation",
"Purchase Order", "Purchase Order",
@@ -985,10 +991,4 @@ def add_party_account(party_type, party, company, account):
def render_address(address, check_permissions=True): def render_address(address, check_permissions=True):
try: return frappe.call(_render_address, address, check_permissions=check_permissions)
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)

View File

@@ -1004,7 +1004,7 @@ class ReceivablePayableReport:
def get_columns(self): def get_columns(self):
self.columns = [] self.columns = []
self.add_column(_("Posting Date"), fieldtype="Date") self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date")
self.add_column( self.add_column(
label=_("Party Type"), label=_("Party Type"),
fieldname="party_type", fieldname="party_type",
@@ -1018,8 +1018,15 @@ class ReceivablePayableReport:
options="party_type", options="party_type",
width=180, 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( self.add_column(
label=self.account_type + " Account", label=label,
fieldname="party_account", fieldname="party_account",
fieldtype="Link", fieldtype="Link",
options="Account", options="Account",
@@ -1057,7 +1064,7 @@ class ReceivablePayableReport:
width=180, 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": if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")

View File

@@ -7,6 +7,7 @@ from frappe import _
from frappe.utils import cint, flt from frappe.utils import cint, flt
from erpnext.accounts.report.financial_statements import ( from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,
get_columns, get_columns,
get_data, get_data,
get_filtered_list_for_consolidated_report, get_filtered_list_for_consolidated_report,
@@ -101,6 +102,9 @@ def execute(filters=None):
period_list, asset, liability, equity, provisional_profit_loss, currency, filters 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 return columns, data, message, chart, report_summary, primitive_summary

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import copy
import functools import functools
import math import math
import re import re
@@ -653,3 +654,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list):
filtered_summary_list.append(period) filtered_summary_list.append(period)
return filtered_summary_list 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

View File

@@ -402,10 +402,10 @@ class GrossProfitGenerator:
self.load_invoice_items() self.load_invoice_items()
self.get_delivery_notes() self.get_delivery_notes()
self.load_product_bundle()
if filters.group_by == "Invoice": if filters.group_by == "Invoice":
self.group_items_by_invoice() self.group_items_by_invoice()
self.load_product_bundle()
self.load_non_stock_items() self.load_non_stock_items()
self.get_returned_invoice_items() self.get_returned_invoice_items()
self.process() self.process()
@@ -617,6 +617,7 @@ class GrossProfitGenerator:
if packed_item.get("parent_detail_docname") == row.item_row: if packed_item.get("parent_detail_docname") == row.item_row:
packed_item_row = row.copy() packed_item_row = row.copy()
packed_item_row.warehouse = packed_item.warehouse 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) buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
return flt(buying_amount, self.currency_precision) return flt(buying_amount, self.currency_precision)
@@ -649,7 +650,9 @@ class GrossProfitGenerator:
else: else:
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
if (row.update_stock or row.dn_detail) and my_sle: 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: if row.dn_detail:
parenttype, parent = "Delivery Note", row.delivery_note parenttype, parent = "Delivery Note", row.delivery_note
@@ -797,6 +800,7 @@ class GrossProfitGenerator:
`tabSales Invoice`.project, `tabSales Invoice`.update_stock, `tabSales Invoice`.project, `tabSales Invoice`.update_stock,
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer, `tabSales Invoice`.customer_group,
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, `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`.item_name, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail, `tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
@@ -857,6 +861,7 @@ class GrossProfitGenerator:
""" """
grouped = OrderedDict() grouped = OrderedDict()
product_bundles = self.product_bundles.get("Sales Invoice", {})
for row in self.si_list: for row in self.si_list:
# initialize list with a header row for each new parent # 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 item is a bundle, add it's components as seperate rows
if frappe.db.exists("Product Bundle", row.item_code): if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code):
bundled_items = self.get_bundle_items(row)
for x in bundled_items: for x in bundled_items:
bundle_item = self.get_bundle_item_row(row, x) bundle_item = self.get_bundle_item_row(row, x)
grouped.get(row.parent).append(bundle_item) grouped.get(row.parent).append(bundle_item)
@@ -904,47 +908,40 @@ class GrossProfitGenerator:
"item_row": None, "item_row": None,
"is_return": row.is_return, "is_return": row.is_return,
"cost_center": row.cost_center, "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): def get_bundle_item_row(self, row, item):
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)
return frappe._dict( return frappe._dict(
{ {
"parent_invoice": product_bundle.item_code, "parent_invoice": row.item_code,
"indent": product_bundle.indent + 1, "parenttype": row.parenttype,
"indent": row.indent + 1,
"parent": None, "parent": None,
"invoice_or_item": item.item_code, "invoice_or_item": item.item_code,
"posting_date": product_bundle.posting_date, "posting_date": row.posting_date,
"posting_time": product_bundle.posting_time, "posting_time": row.posting_time,
"project": product_bundle.project, "project": row.project,
"customer": product_bundle.customer, "customer": row.customer,
"customer_group": product_bundle.customer_group, "customer_group": row.customer_group,
"item_code": item.item_code, "item_code": item.item_code,
"item_name": item_name, "item_name": item.item_name,
"description": description, "description": item.description,
"warehouse": product_bundle.warehouse, "warehouse": item.warehouse or row.warehouse,
"item_group": item_group, "update_stock": row.update_stock,
"brand": brand, "item_group": "",
"dn_detail": product_bundle.dn_detail, "brand": "",
"delivery_note": product_bundle.delivery_note, "dn_detail": row.dn_detail,
"qty": (flt(product_bundle.qty) * flt(item.qty)), "delivery_note": row.delivery_note,
"item_row": None, "qty": item.total_qty * -1,
"is_return": product_bundle.is_return, "item_row": row.item_row,
"cost_center": product_bundle.cost_center, "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): def get_stock_ledger_entries(self, item_code, warehouse):
if item_code and warehouse: if item_code and warehouse:
if (item_code, warehouse) not in self.sle: if (item_code, warehouse) not in self.sle:

View File

@@ -7,6 +7,8 @@ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from erpnext.accounts.report.financial_statements import ( from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,
compute_margin_view_data,
get_columns, get_columns,
get_data, get_data,
get_filtered_list_for_consolidated_report, 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 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 return columns, data, None, chart, report_summary, primitive_summary

View File

@@ -2313,6 +2313,12 @@ class AccountsController(TransactionBase):
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
primary_account_currency = get_account_currency(primary_account) primary_account_currency = get_account_currency(primary_account)
secondary_account_currency = get_account_currency(secondary_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 = frappe.new_doc("Journal Entry")
jv.voucher_type = "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.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes" advance_entry.is_advance = "Yes"
# update dimesions # Update dimensions
dimensions_dict = frappe._dict() dimensions_dict = frappe._dict()
active_dimensions = get_dimensions()[0] active_dimensions = get_dimensions()[0]
for dim in active_dimensions: for dim in active_dimensions:
@@ -2346,17 +2352,58 @@ class AccountsController(TransactionBase):
reconcilation_entry.update(dimensions_dict) reconcilation_entry.update(dimensions_dict)
advance_entry.update(dimensions_dict) advance_entry.update(dimensions_dict)
if self.doctype == "Sales Invoice": # Calculate exchange rates if necessary
reconcilation_entry.credit_in_account_currency = self.outstanding_amount if multi_currency:
advance_entry.debit_in_account_currency = self.outstanding_amount # 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: else:
advance_entry.credit_in_account_currency = self.outstanding_amount if self.doctype == "Sales Invoice":
reconcilation_entry.debit_in_account_currency = self.outstanding_amount reconcilation_entry.credit_in_account_currency = self.outstanding_amount
advance_entry.debit_in_account_currency = self.outstanding_amount
default_currency = erpnext.get_company_currency(self.company) else:
if primary_account_currency != default_currency or secondary_account_currency != default_currency: advance_entry.credit_in_account_currency = self.outstanding_amount
jv.multi_currency = 1 reconcilation_entry.debit_in_account_currency = self.outstanding_amount
jv.multi_currency = multi_currency
jv.append("accounts", reconcilation_entry) jv.append("accounts", reconcilation_entry)
jv.append("accounts", advance_entry) jv.append("accounts", advance_entry)

View File

@@ -68,19 +68,13 @@ class SellingController(StockController):
if customer: if customer:
from erpnext.accounts.party import _get_party_details 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( party_details = _get_party_details(
customer, customer,
ignore_permissions=self.flags.ignore_permissions, ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype, doctype=self.doctype,
company=self.company, company=self.company,
posting_date=self.get("posting_date"), 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, party_address=self.customer_address,
shipping_address=self.shipping_address_name, shipping_address=self.shipping_address_name,
company_address=self.get("company_address"), company_address=self.get("company_address"),
@@ -365,12 +359,32 @@ class SellingController(StockController):
return il return il
def has_product_bundle(self, item_code): def has_product_bundle(self, item_code):
product_bundle = frappe.qb.DocType("Product Bundle") product_bundle_items = getattr(self, "_product_bundle_items", None)
return ( if product_bundle_items is None:
frappe.qb.from_(product_bundle) self._product_bundle_items = product_bundle_items = {}
.select(product_bundle.name)
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) if item_code not in product_bundle_items:
).run() 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): def get_already_delivered_qty(self, current_docname, so, so_detail):
delivered_via_dn = frappe.db.sql( delivered_via_dn = frappe.db.sql(

View File

@@ -89,10 +89,18 @@ class WorkOrder(Document):
self.status = self.get_status() self.status = self.get_status()
self.validate_workstation_type() self.validate_workstation_type()
if self.source_warehouse:
self.set_warehouses()
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
self.set_required_items(reset_only_qty=len(self.get("required_items"))) 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): def validate_workstation_type(self):
for row in self.operations: for row in self.operations:
if not row.workstation and not row.workstation_type: if not row.workstation and not row.workstation_type:

View File

@@ -421,7 +421,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
setup_sms() { setup_sms() {
var me = this; var me = this;
let blacklist = ['Purchase Invoice', 'BOM']; 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)) { && !blacklist.includes(this.frm.doctype)) {
this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); }); 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) { apply_discount_on_item(doc, cdt, cdn, field) {
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if(!item.price_list_rate) { if(item && !item.price_list_rate) {
item[field] = 0.0; item[field] = 0.0;
} else { } else {
this.price_list_rate(doc, cdt, cdn); this.price_list_rate(doc, cdt, cdn);

View File

@@ -9,40 +9,29 @@ erpnext.financial_statements = {
data && data &&
column.colIndex >= 3 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 growthPercent = data[column.fieldname];
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 = Math.round(annualGrowth * 10000) / 100; //calculating the rounded off percentage if (growthPercent == undefined) return "NA"; //making this not applicable for undefined/null values
value = $(`<span>${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}</span>`); if (column.fieldname === "total") {
if (growthPercent < 0) { value = $(`<span>${growthPercent}</span>`);
value = $(value).addClass("text-danger");
} else { } else {
value = $(value).addClass("text-success"); value = $(`<span>${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}</span>`);
if (growthPercent < 0) {
value = $(value).addClass("text-danger");
} else {
value = $(value).addClass("text-success");
}
} }
value = $(value).wrap("<p></p>").parent().html(); value = $(value).wrap("<p></p>").parent().html();
return value; return value;
} else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) { } 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) { 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 marginPercent = data[column.fieldname];
const currentAnnualvalue = data[column.fieldname];
const baseValue = this.baseData[column.colIndex].content; if (marginPercent == undefined) return "NA"; //making this not applicable for undefined/null values
if (currentAnnualvalue == undefined || baseValue <= 0) return "NA";
const marginPercent = Math.round((currentAnnualvalue / baseValue) * 10000) / 100;
value = $(`<span>${marginPercent + "%"}</span>`); value = $(`<span>${marginPercent + "%"}</span>`);
if (marginPercent < 0) value = $(value).addClass("text-danger"); if (marginPercent < 0) value = $(value).addClass("text-danger");

View File

@@ -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) { address_query: function (doc) {
if (frappe.dynamic_link) { if (frappe.dynamic_link) {
if (!doc[frappe.dynamic_link.fieldname]) { if (!doc[frappe.dynamic_link.fieldname]) {

View File

@@ -95,8 +95,9 @@
"shipping_address", "shipping_address",
"company_address_section", "company_address_section",
"company_address", "company_address",
"column_break_87",
"company_address_display", "company_address_display",
"column_break_87",
"company_contact_person",
"terms_tab", "terms_tab",
"payment_schedule_section", "payment_schedule_section",
"payment_terms_template", "payment_terms_template",
@@ -1066,13 +1067,20 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"fieldname": "company_contact_person",
"fieldtype": "Link",
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"idx": 82, "idx": 82,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-20 16:04:21.567847", "modified": "2024-11-26 12:43:29.293637",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation", "name": "Quotation",

View File

@@ -455,7 +455,9 @@ def _make_customer(source_name, ignore_permissions=False):
raise raise
except frappe.MandatoryError as e: except frappe.MandatoryError as e:
mandatory_fields = e.args[0].split(":")[1].split(",") 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 = [] frappe.local.message_log = []
lead_link = frappe.utils.get_link_to_form("Lead", lead_name) lead_link = frappe.utils.get_link_to_form("Lead", lead_name)

View File

@@ -112,8 +112,9 @@
"dispatch_address", "dispatch_address",
"col_break46", "col_break46",
"company_address", "company_address",
"column_break_92",
"company_address_display", "company_address_display",
"column_break_92",
"company_contact_person",
"payment_schedule_section", "payment_schedule_section",
"payment_terms_section", "payment_terms_section",
"payment_terms_template", "payment_terms_template",
@@ -1626,13 +1627,20 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"fieldname": "company_contact_person",
"fieldtype": "Link",
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-05-23 16:35:54.905804", "modified": "2024-11-26 12:42:06.872527",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@@ -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('shipping_address_name', erpnext.queries.address_query);
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_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_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); erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);

View File

@@ -109,8 +109,9 @@
"dispatch_address", "dispatch_address",
"company_address_section", "company_address_section",
"company_address", "company_address",
"column_break_101",
"company_address_display", "company_address_display",
"column_break_101",
"company_contact_person",
"terms_tab", "terms_tab",
"tc_name", "tc_name",
"terms", "terms",
@@ -1395,13 +1396,20 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"fieldname": "company_contact_person",
"fieldtype": "Link",
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
} }
], ],
"icon": "fa fa-truck", "icon": "fa fa-truck",
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-20 16:05:02.854990", "modified": "2024-11-26 12:44:28.258215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@@ -907,7 +907,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
if adjust_incoming_rate: if adjust_incoming_rate:
adjusted_amt = 0.0 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) adjusted_amt = flt(item.billed_amt) - flt(item.amount)
item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False) item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False)

View File

@@ -194,11 +194,11 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
if isinstance(qty_fields, str): if isinstance(qty_fields, str):
qty_fields = [qty_fields] qty_fields = [qty_fields]
distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children())) distinct_uoms = tuple(set(uom for uom in (d.get(uom_field) for d in doc.get_all_children()) if uom))
integer_uoms = list( integer_uoms = set(
filter( d[0]
lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None, for d in frappe.db.get_values(
distinct_uoms, "UOM", (("name", "in", distinct_uoms), ("must_be_whole_number", "=", 1)), cache=True
) )
) )