diff --git a/README.md b/README.md
index 4f65ceb70bd6..953f6c52e2be 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,75 @@
-
-
-
-
-
-
+
+
+# ERPNext
+
+100% Open-Source ERP system to help you run your business.
+
+### Key Features
+
+- **Accounting**: All the tools you need to manage cash flow in one place, right from recording transactions to summarizing and analyzing financial reports.
+- **Order Management**: Track inventory levels, replenish stock, and manage sales orders, customers, suppliers, shipments, deliverables, and order fulfillment.
+- **Manufacturing**: Simplifies the production cycle, helps track material consumption, exhibits capacity planning, handles subcontracting, and more!
+- **Asset Management**: From purchase to perishment, IT infrastructure to equipment. Cover every branch of your organization, all in one centralized system.
+- **Projects**: Delivery both internal and external Projects on time, budget and Profitability. Track tasks, timesheets, and issues by project.
+
+
And More
+
+### Under the Hood
+
+- [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework written in Python and Javascript. The framework provides a robust foundation for building web applications, including a database abstraction layer, user authentication, and a REST API.
+
+- [**Frappe UI**](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface. The Frappe UI library provides a variety of components that can be used to build single-page applications on top of the Frappe Framework.
+
+## Production Setup
+
+### Managed Hosting
+
+You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications with peace of mind.
+
+It takes care of installation, setup, upgrades, monitoring, maintenance and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments.
+
+
-> Login for the PWD site: (username: Administrator, password: admin)
-### Containerized Installation
+
+### Self-hosting
+#### Docker
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
@@ -59,6 +80,35 @@ The Easy Way: our install script for bench will install all dependencies (e.g. M
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
+### Local
+
+To setup the repository locally follow the steps mentioned below:
+
+1. Setup bench by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation) and start the server
+ ```
+ bench start
+ ```
+
+2. In a separate terminal window, run the following commands:
+ ```
+ # Create a new site
+ bench new-site erpnext.dev
+
+ # Map your site to localhost
+ bench --site erpnext.dev add-to-hosts
+ ```
+
+3. Get the ERPNext app and install it
+ ```
+ # Get the ERPNext app
+ bench get-app https://github.com/frappe/erpnext
+
+ # Install the app
+ bench --site erpnext.dev install-app erpnext
+ ```
+
+4. Open the URL `http://erpnext.dev:8000/app` in your browser, you should see the app running
+
## Learning and community
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
@@ -73,14 +123,18 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
1. [Report Security Vulnerabilities](https://erpnext.com/security)
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
-## License
-
-GNU/General Public License (see [license.txt](license.txt))
-
-The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
-
-By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
## Logo and Trademark Policy
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
+
+
+
+
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 83186bc1d24b..d8eea29bfc03 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -651,8 +651,17 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
qty = pricing_rule.free_qty or 1
if pricing_rule.is_recursive:
- transaction_qty = (args.get("qty") if args else doc.total_qty) - pricing_rule.apply_recursion_over
- if transaction_qty:
+ transaction_qty = sum(
+ [
+ row.qty
+ for row in doc.items
+ if not row.is_free_item
+ and row.item_code == args.item_code
+ and row.pricing_rules == args.pricing_rules
+ ]
+ )
+ transaction_qty = transaction_qty - pricing_rule.apply_recursion_over
+ if transaction_qty and transaction_qty > 0:
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
if pricing_rule.round_free_qty:
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index cdd5baf32409..bec5d128f0a3 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -28,15 +28,14 @@ def get_group_by_asset_category_data(filters):
for asset_category in asset_categories:
row = frappe._dict()
- # row.asset_category = asset_category
row.update(asset_category)
- row.cost_as_on_to_date = (
- flt(row.cost_as_on_from_date)
- + flt(row.cost_of_new_purchase)
- - flt(row.cost_of_sold_asset)
- - flt(row.cost_of_scrapped_asset)
- - flt(row.cost_of_capitalized_asset)
+ row.value_as_on_to_date = (
+ flt(row.value_as_on_from_date)
+ + flt(row.value_of_new_purchase)
+ - flt(row.value_of_sold_asset)
+ - flt(row.value_of_scrapped_asset)
+ - flt(row.value_of_capitalized_asset)
)
row.update(
@@ -53,11 +52,11 @@ def get_group_by_asset_category_data(filters):
- flt(row.depreciation_eliminated_during_the_period)
)
- row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
+ row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
- row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
+ row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
@@ -85,12 +84,12 @@ def get_asset_categories_for_grouped_by_category(filters):
end
else
0
- end), 0) as cost_as_on_from_date,
+ end), 0) as value_as_on_from_date,
ifnull(sum(case when a.purchase_date >= %(from_date)s then
a.gross_purchase_amount
else
0
- end), 0) as cost_of_new_purchase,
+ end), 0) as value_of_new_purchase,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
@@ -101,7 +100,7 @@ def get_asset_categories_for_grouped_by_category(filters):
end
else
0
- end), 0) as cost_of_sold_asset,
+ end), 0) as value_of_sold_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
@@ -112,7 +111,7 @@ def get_asset_categories_for_grouped_by_category(filters):
end
else
0
- end), 0) as cost_of_scrapped_asset,
+ end), 0) as value_of_scrapped_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
@@ -123,7 +122,7 @@ def get_asset_categories_for_grouped_by_category(filters):
end
else
0
- end), 0) as cost_of_capitalized_asset
+ end), 0) as value_of_capitalized_asset
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
and not exists(
@@ -164,12 +163,12 @@ def get_asset_details_for_grouped_by_category(filters):
end
else
0
- end), 0) as cost_as_on_from_date,
+ end), 0) as value_as_on_from_date,
ifnull(sum(case when a.purchase_date >= %(from_date)s then
a.gross_purchase_amount
else
0
- end), 0) as cost_of_new_purchase,
+ end), 0) as value_of_new_purchase,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
@@ -180,7 +179,7 @@ def get_asset_details_for_grouped_by_category(filters):
end
else
0
- end), 0) as cost_of_sold_asset,
+ end), 0) as value_of_sold_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
@@ -191,7 +190,7 @@ def get_asset_details_for_grouped_by_category(filters):
end
else
0
- end), 0) as cost_of_scrapped_asset,
+ end), 0) as value_of_scrapped_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
@@ -202,7 +201,7 @@ def get_asset_details_for_grouped_by_category(filters):
end
else
0
- end), 0) as cost_of_capitalized_asset
+ end), 0) as value_of_capitalized_asset
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
and not exists(
@@ -232,15 +231,14 @@ def get_group_by_asset_data(filters):
for asset_detail in asset_details:
row = frappe._dict()
- # row.asset_category = asset_category
row.update(asset_detail)
- row.cost_as_on_to_date = (
- flt(row.cost_as_on_from_date)
- + flt(row.cost_of_new_purchase)
- - flt(row.cost_of_sold_asset)
- - flt(row.cost_of_scrapped_asset)
- - flt(row.cost_of_capitalized_asset)
+ row.value_as_on_to_date = (
+ flt(row.value_as_on_from_date)
+ + flt(row.value_of_new_purchase)
+ - flt(row.value_of_sold_asset)
+ - flt(row.value_of_scrapped_asset)
+ - flt(row.value_of_capitalized_asset)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
@@ -251,11 +249,11 @@ def get_group_by_asset_data(filters):
- flt(row.depreciation_eliminated_during_the_period)
)
- row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
+ row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
- row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
+ row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
@@ -446,38 +444,38 @@ def get_columns(filters):
columns += [
{
- "label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
- "fieldname": "cost_as_on_from_date",
+ "label": _("Value as on") + " " + formatdate(filters.day_before_from_date),
+ "fieldname": "value_as_on_from_date",
"fieldtype": "Currency",
"width": 140,
},
{
- "label": _("Cost of New Purchase"),
- "fieldname": "cost_of_new_purchase",
+ "label": _("Value of New Purchase"),
+ "fieldname": "value_of_new_purchase",
"fieldtype": "Currency",
"width": 140,
},
{
- "label": _("Cost of Sold Asset"),
- "fieldname": "cost_of_sold_asset",
+ "label": _("Value of Sold Asset"),
+ "fieldname": "value_of_sold_asset",
"fieldtype": "Currency",
"width": 140,
},
{
- "label": _("Cost of Scrapped Asset"),
- "fieldname": "cost_of_scrapped_asset",
+ "label": _("Value of Scrapped Asset"),
+ "fieldname": "value_of_scrapped_asset",
"fieldtype": "Currency",
"width": 140,
},
{
- "label": _("Cost of New Capitalized Asset"),
- "fieldname": "cost_of_capitalized_asset",
+ "label": _("Value of New Capitalized Asset"),
+ "fieldname": "value_of_capitalized_asset",
"fieldtype": "Currency",
"width": 140,
},
{
- "label": _("Cost as on") + " " + formatdate(filters.to_date),
- "fieldname": "cost_as_on_to_date",
+ "label": _("Value as on") + " " + formatdate(filters.to_date),
+ "fieldname": "value_as_on_to_date",
"fieldtype": "Currency",
"width": 140,
},
diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js
index 3600db852f8c..847eff3a05a4 100644
--- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js
+++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.js
@@ -92,5 +92,27 @@ frappe.query_reports["Customer Ledger Summary"] = {
fieldtype: "Data",
hidden: 1,
},
+ {
+ fieldname: "cost_center",
+ label: __("Cost Center"),
+ fieldtype: "MultiSelectList",
+ get_data: function (txt) {
+ return frappe.db.get_link_options("Cost Center", txt, {
+ company: frappe.query_report.get_filter_value("company"),
+ });
+ },
+ },
+ {
+ fieldname: "project",
+ label: __("Project"),
+ fieldtype: "MultiSelectList",
+ get_data: function (txt) {
+ return frappe.db.get_link_options("Project", txt, {
+ company: frappe.query_report.get_filter_value("company"),
+ });
+ },
+ },
],
};
+
+erpnext.utils.add_dimensions("Customer Ledger Summary", 14);
diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
index 7b990965b69f..f11c4c24863c 100644
--- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
+++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
@@ -4,8 +4,15 @@
import frappe
from frappe import _, qb, scrub
+from frappe.query_builder.functions import IfNull
from frappe.utils import getdate, nowdate
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+ get_accounting_dimensions,
+ get_dimension_with_children,
+)
+from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
+
class PartyLedgerSummaryReport:
def __init__(self, filters=None):
@@ -246,95 +253,143 @@ def get_data(self):
return out
def get_gl_entries(self):
- conditions = self.prepare_conditions()
- join = join_field = ""
+ gle = qb.DocType("GL Entry")
+ query = (
+ qb.from_(gle)
+ .select(
+ gle.posting_date,
+ gle.party,
+ gle.voucher_type,
+ gle.voucher_no,
+ gle.against_voucher_type,
+ gle.against_voucher,
+ gle.debit,
+ gle.credit,
+ gle.is_opening,
+ )
+ .where(
+ (gle.docstatus < 2)
+ & (gle.is_cancelled == 0)
+ & (gle.party_type == self.filters.party_type)
+ & (IfNull(gle.party, "") != "")
+ & (gle.posting_date <= self.filters.to_date)
+ )
+ .orderby(gle.posting_date)
+ )
+
if self.filters.party_type == "Customer":
- join_field = ", p.customer_name as party_name"
- join = "left join `tabCustomer` p on gle.party = p.name"
+ customer = qb.DocType("Customer")
+ query = (
+ query.select(customer.customer_name.as_("party_name"))
+ .left_join(customer)
+ .on(customer.name == gle.party)
+ )
elif self.filters.party_type == "Supplier":
- join_field = ", p.supplier_name as party_name"
- join = "left join `tabSupplier` p on gle.party = p.name"
-
- self.gl_entries = frappe.db.sql(
- f"""
- select
- gle.posting_date, gle.party, gle.voucher_type, gle.voucher_no, gle.against_voucher_type,
- gle.against_voucher, gle.debit, gle.credit, gle.is_opening {join_field}
- from `tabGL Entry` gle
- {join}
- where
- gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != ''
- and gle.posting_date <= %(to_date)s {conditions}
- order by gle.posting_date
- """,
- self.filters,
- as_dict=True,
- )
+ supplier = qb.DocType("Supplier")
+ query = (
+ query.select(supplier.supplier_name.as_("party_name"))
+ .left_join(supplier)
+ .on(supplier.name == gle.party)
+ )
- def prepare_conditions(self):
- conditions = [""]
+ query = self.prepare_conditions(query)
+ self.gl_entries = query.run(as_dict=True)
+ def prepare_conditions(self, query):
+ gle = qb.DocType("GL Entry")
if self.filters.company:
- conditions.append("gle.company=%(company)s")
+ query = query.where(gle.company == self.filters.company)
if self.filters.finance_book:
- conditions.append("ifnull(finance_book,'') in (%(finance_book)s, '')")
+ query = query.where(IfNull(gle.finance_book, "") == self.filters.finance_book)
- if self.filters.get("party"):
- conditions.append("party=%(party)s")
+ if self.filters.party:
+ query = query.where(gle.party == self.filters.party)
if self.filters.party_type == "Customer":
- if self.filters.get("customer_group"):
- lft, rgt = frappe.get_cached_value(
- "Customer Group", self.filters["customer_group"], ["lft", "rgt"]
+ customer = qb.DocType("Customer")
+ if self.filters.customer_group:
+ query = query.where(
+ (gle.party).isin(
+ qb.from_(customer)
+ .select(customer.name)
+ .where(customer.customer_group == self.filters.customer_group)
+ )
)
- conditions.append(
- f"""party in (select name from tabCustomer
- where exists(select name from `tabCustomer Group` where lft >= {lft} and rgt <= {rgt}
- and name=tabCustomer.customer_group))"""
+ if self.filters.territory:
+ query = query.where(
+ (gle.party).isin(
+ qb.from_(customer)
+ .select(customer.name)
+ .where(customer.territory == self.filters.territory)
+ )
)
- if self.filters.get("territory"):
- lft, rgt = frappe.db.get_value("Territory", self.filters.get("territory"), ["lft", "rgt"])
-
- conditions.append(
- f"""party in (select name from tabCustomer
- where exists(select name from `tabTerritory` where lft >= {lft} and rgt <= {rgt}
- and name=tabCustomer.territory))"""
+ if self.filters.payment_terms_template:
+ query = query.where(
+ (gle.party).isin(
+ qb.from_(customer)
+ .select(customer.name)
+ .where(customer.payment_terms == self.filters.payment_terms_template)
+ )
)
- if self.filters.get("payment_terms_template"):
- conditions.append(
- "party in (select name from tabCustomer where payment_terms=%(payment_terms_template)s)"
+ if self.filters.sales_partner:
+ query = query.where(
+ (gle.party).isin(
+ qb.from_(customer)
+ .select(customer.name)
+ .where(customer.default_sales_partner == self.filters.sales_partner)
+ )
)
- if self.filters.get("sales_partner"):
- conditions.append(
- "party in (select name from tabCustomer where default_sales_partner=%(sales_partner)s)"
+ if self.filters.sales_person:
+ sales_team = qb.DocType("Sales Team")
+ query = query.where(
+ (gle.party).isin(
+ qb.from_(sales_team)
+ .select(sales_team.parent)
+ .where(sales_team.sales_person == self.filters.sales_person)
+ )
)
- if self.filters.get("sales_person"):
- lft, rgt = frappe.db.get_value(
- "Sales Person", self.filters.get("sales_person"), ["lft", "rgt"]
+ if self.filters.party_type == "Supplier":
+ if self.filters.supplier_group:
+ supplier = qb.DocType("Supplier")
+ query = query.where(
+ (gle.party).isin(
+ qb.from_(supplier)
+ .select(supplier.name)
+ .where(supplier.supplier_group == self.filters.supplier_group)
+ )
)
- conditions.append(
- """exists(select name from `tabSales Team` steam where
- steam.sales_person in (select name from `tabSales Person` where lft >= {} and rgt <= {})
- and ((steam.parent = voucher_no and steam.parenttype = voucher_type)
- or (steam.parent = against_voucher and steam.parenttype = against_voucher_type)
- or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)
- )
+ if self.filters.cost_center:
+ self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
+ query = query.where((gle.cost_center).isin(self.filters.cost_center))
- if self.filters.party_type == "Supplier":
- if self.filters.get("supplier_group"):
- conditions.append(
- """party in (select name from tabSupplier
- where supplier_group=%(supplier_group)s)"""
- )
+ if self.filters.project:
+ query = query.where((gle.project).isin(self.filters.project))
- return " and ".join(conditions)
+ accounting_dimensions = get_accounting_dimensions(as_list=False)
+
+ if accounting_dimensions:
+ for dimension in accounting_dimensions:
+ if self.filters.get(dimension.fieldname):
+ if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
+ self.filters[dimension.fieldname] = get_dimension_with_children(
+ dimension.document_type, self.filters.get(dimension.fieldname)
+ )
+ query = query.where(
+ (gle[dimension.fieldname]).isin(self.filters.get(dimension.fieldname))
+ )
+ else:
+ query = query.where(
+ (gle[dimension.fieldname]).isin(self.filters.get(dimension.fieldname))
+ )
+
+ return query
def get_return_invoices(self):
doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice"
@@ -351,53 +406,45 @@ def get_return_invoices(self):
]
def get_party_adjustment_amounts(self):
- conditions = self.prepare_conditions()
account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
- income_or_expense_accounts = frappe.db.get_all(
+ self.income_or_expense_accounts = frappe.db.get_all(
"Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
)
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
- gl = qb.DocType("GL Entry")
- if not income_or_expense_accounts:
+ if not self.income_or_expense_accounts:
# prevent empty 'in' condition
- income_or_expense_accounts.append("")
+ self.income_or_expense_accounts.append("")
else:
# escape '%' in account name
# ignoring frappe.db.escape as it replaces single quotes with double quotes
- income_or_expense_accounts = [x.replace("%", "%%") for x in income_or_expense_accounts]
+ self.income_or_expense_accounts = [x.replace("%", "%%") for x in self.income_or_expense_accounts]
+
+ gl = qb.DocType("GL Entry")
+ accounts_query = self.get_base_accounts_query()
+ accounts_query_voucher_no = accounts_query.select(gl.voucher_no)
+ accounts_query_voucher_type = accounts_query.select(gl.voucher_type)
- accounts_query = (
+ subquery = self.get_base_subquery()
+ subquery_voucher_no = subquery.select(gl.voucher_no)
+ subquery_voucher_type = subquery.select(gl.voucher_type)
+
+ gl_entries = (
qb.from_(gl)
- .select(gl.voucher_type, gl.voucher_no)
+ .select(
+ gl.posting_date, gl.account, gl.party, gl.voucher_type, gl.voucher_no, gl.debit, gl.credit
+ )
.where(
- (gl.account.isin(income_or_expense_accounts))
- & (gl.posting_date.gte(self.filters.from_date))
- & (gl.posting_date.lte(self.filters.to_date))
+ (gl.docstatus < 2)
+ & (gl.is_cancelled == 0)
+ & (gl.voucher_no.isin(accounts_query_voucher_no))
+ & (gl.voucher_type.isin(accounts_query_voucher_type))
+ & (gl.voucher_no.isin(subquery_voucher_no))
+ & (gl.voucher_type.isin(subquery_voucher_type))
)
- )
-
- gl_entries = frappe.db.sql(
- f"""
- select
- posting_date, account, party, voucher_type, voucher_no, debit, credit
- from
- `tabGL Entry`
- where
- docstatus < 2 and is_cancelled = 0
- and (voucher_type, voucher_no) in (
- {accounts_query}
- ) and (voucher_type, voucher_no) in (
- select voucher_type, voucher_no from `tabGL Entry` gle
- where gle.party_type=%(party_type)s and ifnull(party, '') != ''
- and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions}
- )
- """,
- self.filters,
- as_dict=True,
- )
+ ).run(as_dict=True)
self.party_adjustment_details = {}
self.party_adjustment_accounts = set()
@@ -439,6 +486,26 @@ def get_party_adjustment_amounts(self):
self.party_adjustment_details[party].setdefault(account, 0)
self.party_adjustment_details[party][account] += amount
+ def get_base_accounts_query(self):
+ gl = qb.DocType("GL Entry")
+ query = qb.from_(gl).where(
+ (gl.account.isin(self.income_or_expense_accounts))
+ & (gl.posting_date.gte(self.filters.from_date))
+ & (gl.posting_date.lte(self.filters.to_date))
+ )
+ return query
+
+ def get_base_subquery(self):
+ gl = qb.DocType("GL Entry")
+ query = qb.from_(gl).where(
+ (gl.docstatus < 2)
+ & (gl.party_type == self.filters.party_type)
+ & (IfNull(gl.party, "") != "")
+ & (gl.posting_date.between(self.filters.from_date, self.filters.to_date))
+ )
+ query = self.prepare_conditions(query)
+ return query
+
def execute(filters=None):
args = {
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index fdaf3fe2a598..6b0b6d0b1e9d 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -35,9 +35,6 @@ def execute(filters=None):
if filters.get("party"):
filters.party = frappe.parse_json(filters.get("party"))
- if filters.get("voucher_no") and not filters.get("group_by"):
- filters.group_by = "Group by Voucher (Consolidated)"
-
validate_filters(filters, account_details)
validate_party(filters)
@@ -373,16 +370,21 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
if acc_dict.entries:
# opening
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
- if filters.get("group_by") != "Group by Voucher":
+ if (not filters.get("group_by") and not filters.get("voucher_no")) or (
+ filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
+ ):
data.append(acc_dict.totals.opening)
data += acc_dict.entries
# totals
- data.append(acc_dict.totals.total)
+ if filters.get("group_by") or not filters.voucher_no:
+ data.append(acc_dict.totals.total)
# closing
- if filters.get("group_by") != "Group by Voucher":
+ if (not filters.get("group_by") and not filters.get("voucher_no")) or (
+ filters.get("group_by") and filters.get("group_by") != "Group by Voucher"
+ ):
data.append(acc_dict.totals.closing)
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index c59a3bd2a7ae..647490dcc905 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -790,7 +790,10 @@ def load_invoice_items(self):
"""
if self.filters.group_by == "Sales Person":
- sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives"
+ sales_person_cols = """, sales.sales_person,
+ sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount,
+ sales.incentives
+ """
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
else:
sales_person_cols = ""
diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
index 5d91575b8b29..4b67331f6bf1 100644
--- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
+++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
@@ -74,5 +74,27 @@ frappe.query_reports["Supplier Ledger Summary"] = {
fieldtype: "Data",
hidden: 1,
},
+ {
+ fieldname: "cost_center",
+ label: __("Cost Center"),
+ fieldtype: "MultiSelectList",
+ get_data: function (txt) {
+ return frappe.db.get_link_options("Cost Center", txt, {
+ company: frappe.query_report.get_filter_value("company"),
+ });
+ },
+ },
+ {
+ fieldname: "project",
+ label: __("Project"),
+ fieldtype: "MultiSelectList",
+ get_data: function (txt) {
+ return frappe.db.get_link_options("Project", txt, {
+ company: frappe.query_report.get_filter_value("company"),
+ });
+ },
+ },
],
};
+
+erpnext.utils.add_dimensions("Supplier Ledger Summary", 11);
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index f2550f738e23..ee651ee9f4c6 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -207,3 +207,23 @@ def clear_old_entries(self):
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
+
+ def create_price_list(self):
+ pl_name = "Mixin Price List"
+ if not frappe.db.exists("Price List", pl_name):
+ self.price_list = (
+ frappe.get_doc(
+ {
+ "doctype": "Price List",
+ "currency": "INR",
+ "enabled": True,
+ "selling": True,
+ "buying": True,
+ "price_list_name": pl_name,
+ }
+ )
+ .insert()
+ .name
+ )
+ else:
+ self.price_list = frappe.get_doc("Price List", pl_name).name
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 9b124826fcec..8715f2b23547 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -805,6 +805,9 @@ def get_depreciation_rate(self, args, on_validate=False):
):
return args.get("rate_of_depreciation")
+ if args.get("rate_of_depreciation") and not flt(args.get("expected_value_after_useful_life")):
+ return args.get("rate_of_depreciation")
+
if self.flags.increase_in_asset_value_due_to_repair:
value = flt(args.get("expected_value_after_useful_life")) / flt(
args.get("value_after_depreciation")
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index 8b00bc29c3cc..60e8778e0e65 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -88,7 +88,8 @@
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
"fieldname": "rate_of_depreciation",
"fieldtype": "Percent",
- "label": "Rate of Depreciation (%)"
+ "label": "Rate of Depreciation (%)",
+ "mandatory_depends_on": "eval:doc.depreciation_method == 'Written Down Value'"
},
{
"fieldname": "salvage_value_percentage",
@@ -128,7 +129,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-11-29 14:36:54.399034",
+ "modified": "2024-12-13 12:11:03.743209",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
diff --git a/erpnext/controllers/tests/test_reactivity.py b/erpnext/controllers/tests/test_reactivity.py
new file mode 100644
index 000000000000..73f7962836c5
--- /dev/null
+++ b/erpnext/controllers/tests/test_reactivity.py
@@ -0,0 +1,69 @@
+import frappe
+from frappe import qb
+from frappe.tests import IntegrationTestCase
+from frappe.utils import getdate, today
+
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import disable_dimension
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestReactivity(AccountsTestMixin, IntegrationTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.create_item()
+ self.create_usd_receivable_account()
+ self.create_price_list()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def disable_dimensions(self):
+ res = frappe.db.get_all("Accounting Dimension", filters={"disabled": False})
+ for x in res:
+ dim = frappe.get_doc("Accounting Dimension", x.name)
+ dim.disabled = True
+ dim.save()
+
+ def test_01_basic_item_details(self):
+ self.disable_dimensions()
+
+ # set Item Price
+ frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "item_code": self.item,
+ "price_list": self.price_list,
+ "price_list_rate": 90,
+ "selling": True,
+ "rate": 90,
+ "valid_from": today(),
+ }
+ ).insert()
+
+ si = frappe.get_doc(
+ {
+ "doctype": "Sales Invoice",
+ "company": self.company,
+ "customer": self.customer,
+ "debit_to": self.debit_to,
+ "posting_date": today(),
+ "cost_center": self.cost_center,
+ "conversion_rate": 1,
+ "selling_price_list": self.price_list,
+ }
+ )
+ itm = si.append("items")
+ itm.item_code = self.item
+ si.process_item_selection(si.items[0].name)
+ self.assertEqual(itm.rate, 90)
+
+ df = qb.DocType("DocField")
+ _res = (
+ qb.from_(df).select(df.fieldname).where(df.parent.eq("Sales Invoice Item") & df.reqd.eq(1)).run()
+ )
+ for field in _res:
+ with self.subTest(field=field):
+ self.assertIsNotNone(itm.get(field[0]))
+ si.save().submit()
diff --git a/erpnext/locale/de.po b/erpnext/locale/de.po
index 317746830a20..be6707c0a72b 100644
--- a/erpnext/locale/de.po
+++ b/erpnext/locale/de.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: info@erpnext.com\n"
"POT-Creation-Date: 2024-12-08 09:35+0000\n"
-"PO-Revision-Date: 2024-12-08 15:15\n"
+"PO-Revision-Date: 2024-12-12 07:07\n"
"Last-Translator: info@erpnext.com\n"
"Language-Team: German\n"
"MIME-Version: 1.0\n"
@@ -13894,7 +13894,7 @@ msgstr "Kurven"
#. Label of the custodian (Link) field in DocType 'Asset'
#: erpnext/assets/doctype/asset/asset.json
msgid "Custodian"
-msgstr "Depotbank"
+msgstr "Betreuer"
#. Label of the custody (Float) field in DocType 'Cashier Closing'
#: erpnext/accounts/doctype/cashier_closing/cashier_closing.json
diff --git a/erpnext/locale/fa.po b/erpnext/locale/fa.po
index 26b7182159b3..74fd488d280e 100644
--- a/erpnext/locale/fa.po
+++ b/erpnext/locale/fa.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: info@erpnext.com\n"
"POT-Creation-Date: 2024-12-08 09:35+0000\n"
-"PO-Revision-Date: 2024-12-08 15:14\n"
+"PO-Revision-Date: 2024-12-11 07:08\n"
"Last-Translator: info@erpnext.com\n"
"Language-Team: Persian\n"
"MIME-Version: 1.0\n"
@@ -297,17 +297,17 @@ msgstr "\"{0}\" باید به ارز شرکت {1} باشد."
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:203
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:106
msgid "(A) Qty After Transaction"
-msgstr "(A) تعداد پس از تراکنش"
+msgstr "(A) مقدار پس از تراکنش"
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:208
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:111
msgid "(B) Expected Qty After Transaction"
-msgstr "(ب) تعداد مورد انتظار پس از تراکنش"
+msgstr "(ب) مقدار مورد انتظار پس از تراکنش"
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:223
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:126
msgid "(C) Total Qty in Queue"
-msgstr "(C) تعداد کل در صف"
+msgstr "(C) مقدار کل در صف"
#: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:184
msgid "(C) Total qty in queue"
@@ -362,12 +362,12 @@ msgstr "(I) نرخ ارزش گذاری"
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:278
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:181
msgid "(J) Valuation Rate as per FIFO"
-msgstr "(J) نرخ ارزیابی مطابق با FIFO"
+msgstr "(J) نرخ ارزش گذاری مطابق با FIFO"
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:288
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:191
msgid "(K) Valuation = Value (D) ÷ Qty (A)"
-msgstr "(K) ارزش = ارزش (D) ÷ تعداد (A)"
+msgstr "(K) ارزشگذاری = ارزش (D) ÷ مقدار (A)"
#. Description of the 'From No' (Int) field in DocType 'Share Transfer'
#. Description of the 'To No' (Int) field in DocType 'Share Transfer'
@@ -1062,14 +1062,14 @@ msgstr "پذیرفته شده"
#. Label of the qty (Float) field in DocType 'Purchase Invoice Item'
#: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
msgid "Accepted Qty"
-msgstr "تعداد پذیرفته شده"
+msgstr "مقدار پذیرفته شده"
#. Label of the stock_qty (Float) field in DocType 'Purchase Invoice Item'
#. Label of the stock_qty (Float) field in DocType 'Purchase Receipt Item'
#: erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
msgid "Accepted Qty in Stock UOM"
-msgstr "تعداد پذیرفته شده در انبار UOM"
+msgstr "مقدار پذیرفته شده در انبار UOM"
#. Label of the qty (Float) field in DocType 'Purchase Receipt Item'
#. Label of the qty (Float) field in DocType 'Subcontracting Receipt Item'
@@ -2664,7 +2664,7 @@ msgstr "افزودن چندگانه"
#: erpnext/projects/doctype/task/task_tree.js:49
msgid "Add Multiple Tasks"
-msgstr ""
+msgstr "افزودن چند تسک"
#. Label of the add_deduct_tax (Select) field in DocType 'Advance Taxes and
#. Charges'
@@ -4943,7 +4943,7 @@ msgstr "یکی دیگر از رکوردهای تخصیص مرکز هزینه {0}
#: erpnext/accounts/doctype/payment_request/payment_request.py:738
msgid "Another Payment Request is already processed"
-msgstr ""
+msgstr "درخواست پرداخت دیگری در حال حاضر پردازش شده است"
#: erpnext/setup/doctype/sales_person/sales_person.py:100
msgid "Another Sales Person {0} exists with the same Employee id"
@@ -7882,7 +7882,7 @@ msgstr "آیتمهای صورتحساب شده برای دریافت"
#: erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py:255
#: erpnext/selling/report/sales_order_analysis/sales_order_analysis.py:276
msgid "Billed Qty"
-msgstr "تعداد صورتحساب"
+msgstr "مقدار صورتحساب شده"
#. Label of the section_break_56 (Section Break) field in DocType 'Purchase
#. Order Item'
@@ -8528,7 +8528,7 @@ msgstr "ساختار درختی را بساز"
#: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py:155
msgid "Buildable Qty"
-msgstr "تعداد قابل ساخت"
+msgstr "مقدار قابل ساخت"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:31
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:44
@@ -19081,7 +19081,7 @@ msgstr "خطا هنگام پردازش حسابداری معوق برای {0}"
#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:400
msgid "Error while reposting item valuation"
-msgstr "خطا هنگام ارسال مجدد ارزیابی مورد"
+msgstr "خطا هنگام ارسال مجدد ارزشگذاری آیتم"
#: erpnext/templates/includes/footer/footer_extension.html:29
msgid "Error: Not a valid id?"
@@ -23004,7 +23004,7 @@ msgstr "در صورت علامت زدن، موجودی در
ارسال ر
#. Description of the 'Scan Mode' (Check) field in DocType 'Pick List'
#: erpnext/stock/doctype/pick_list/pick_list.json
msgid "If checked, picked qty won't automatically be fulfilled on submit of pick list."
-msgstr "اگر علامت زده شود، تعداد انتخاب شده به طور خودکار در ارسال لیست انتخاب انجام نمی شود."
+msgstr "اگر علامت زده شود، مقدار انتخاب شده به طور خودکار در ارسال لیست انتخاب انجام نمی شود."
#. Description of the 'Considered In Paid Amount' (Check) field in DocType
#. 'Purchase Taxes and Charges'
@@ -23061,7 +23061,7 @@ msgstr ""
#. Description of the 'Pick Manually' (Check) field in DocType 'Pick List'
#: erpnext/stock/doctype/pick_list/pick_list.json
msgid "If enabled then system won't override the picked qty / batches / serial numbers."
-msgstr "اگر فعال باشد، سیستم تعداد / دسته / شماره سریال انتخاب شده را بازنویسی نمی کند."
+msgstr "اگر فعال باشد، سیستم مقدار / دسته / شماره سریال انتخاب شده را بازنویسی نمی کند."
#. Description of the 'Send Document Print' (Check) field in DocType 'Request
#. for Quotation'
@@ -23637,7 +23637,7 @@ msgstr "درونبُرد از Google Sheets"
#: erpnext/stock/doctype/item_price/item_price.js:29
msgid "Import in Bulk"
-msgstr "درونبُرد به صورت عمده"
+msgstr "درونبُرد به صورت انبوه"
#: erpnext/edi/doctype/common_code/common_code.py:108
msgid "Importing Common Codes"
@@ -33294,7 +33294,7 @@ msgstr "اطلاعات سفارش"
#: erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py:167
#: erpnext/manufacturing/report/production_planning_report/production_planning_report.py:371
msgid "Order Qty"
-msgstr "تعداد سفارش"
+msgstr "مقدار سفارش"
#. Label of the tracking_section (Section Break) field in DocType 'Purchase
#. Order'
@@ -33364,7 +33364,7 @@ msgstr "مقدار سفارش داده شده"
#: erpnext/manufacturing/doctype/production_plan/production_plan.js:150
msgid "Ordered Qty: Quantity ordered for purchase, but not received."
-msgstr ""
+msgstr "مقدار سفارش: مقدار سفارش داده شده برای خرید، اما دریافت نشده."
#. Label of the ordered_qty (Float) field in DocType 'Blanket Order Item'
#: erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json
@@ -35983,14 +35983,14 @@ msgstr "انتخاب سریال / شماره دسته"
#. Label of the picked_qty (Float) field in DocType 'Packed Item'
#: erpnext/stock/doctype/packed_item/packed_item.json
msgid "Picked Qty"
-msgstr "تعداد انتخاب شده"
+msgstr "مقدار انتخاب شده"
#. Label of the picked_qty (Float) field in DocType 'Sales Order Item'
#. Label of the picked_qty (Float) field in DocType 'Pick List Item'
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
msgid "Picked Qty (in Stock UOM)"
-msgstr "تعداد انتخاب شده (در انبار UOM)"
+msgstr "مقدار انتخاب شده (در انبار UOM)"
#. Option for the 'Pickup Type' (Select) field in DocType 'Shipment'
#: erpnext/stock/doctype/shipment/shipment.json
@@ -40134,7 +40134,7 @@ msgstr " تعداد"
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
msgid "Qty (Company)"
-msgstr ""
+msgstr "مقدار (شرکت)"
#. Label of the actual_qty (Float) field in DocType 'Sales Invoice Item'
#. Label of the actual_qty (Float) field in DocType 'Quotation Item'
@@ -40145,13 +40145,13 @@ msgstr ""
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
msgid "Qty (Warehouse)"
-msgstr ""
+msgstr "مقدار (انبار)"
#. Label of the qty_after_transaction (Float) field in DocType 'Stock Ledger
#. Entry'
#: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
msgid "Qty After Transaction"
-msgstr "تعداد بعد از تراکنش"
+msgstr "مقدار بعد از تراکنش"
#. Label of the required_bom_qty (Float) field in DocType 'Material Request
#. Plan Item'
@@ -40165,7 +40165,7 @@ msgstr "مقدار طبق BOM"
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:188
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:91
msgid "Qty Change"
-msgstr "تغییر تعداد"
+msgstr "تغییر مقدار"
#. Label of the qty_consumed_per_unit (Float) field in DocType 'BOM Explosion
#. Item'
@@ -43015,7 +43015,7 @@ msgstr "درخواست بر اساس تاریخ"
#: erpnext/manufacturing/doctype/workstation/workstation.js:489
msgid "Reqired Qty"
-msgstr ""
+msgstr "مقدار مورد نیاز"
#: erpnext/crm/doctype/opportunity/opportunity.js:89
msgid "Request For Quotation"
@@ -43112,7 +43112,7 @@ msgstr "تعداد درخواستی"
#: erpnext/manufacturing/doctype/production_plan/production_plan.js:150
msgid "Requested Qty: Quantity requested for purchase, but not ordered."
-msgstr ""
+msgstr "مقدار درخواستی: مقدار درخواستی برای خرید، اما سفارش داده نشده."
#: erpnext/buying/report/procurement_tracker/procurement_tracker.py:46
msgid "Requesting Site"
@@ -43271,7 +43271,7 @@ msgstr "ذخیره"
#: erpnext/selling/doctype/sales_order/sales_order.json
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
msgid "Reserve Stock"
-msgstr "ذخیره موجودی"
+msgstr "رزرو موجودی"
#. Label of the reserve_warehouse (Link) field in DocType 'Purchase Order Item
#. Supplied'
@@ -45170,7 +45170,7 @@ msgstr "مقدار س.ف."
#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:107
msgid "SO Total Qty"
-msgstr ""
+msgstr "مقدار کل س.ف"
#: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html:16
#: erpnext/accounts/report/general_ledger/general_ledger.html:60
@@ -46522,7 +46522,7 @@ msgstr "BOM را انتخاب کنید"
#: erpnext/selling/doctype/sales_order/sales_order.js:836
msgid "Select BOM and Qty for Production"
-msgstr "BOM و Qty را برای تولید انتخاب کنید"
+msgstr "BOM و مقدار را برای تولید انتخاب کنید"
#: erpnext/selling/doctype/sales_order/sales_order.js:981
msgid "Select BOM, Qty and For Warehouse"
@@ -56259,7 +56259,7 @@ msgstr "لغو رزرو کنید"
#: erpnext/selling/doctype/sales_order/sales_order.js:483
msgid "Unreserve Stock"
-msgstr "ذخیره موجودی"
+msgstr "لغو رزرو موجودی"
#: erpnext/selling/doctype/sales_order/sales_order.js:495
#: erpnext/stock/doctype/pick_list/pick_list.js:286
@@ -57014,7 +57014,7 @@ msgstr "ارزش گذاری"
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:63
msgid "Valuation (I - K)"
-msgstr ""
+msgstr "ارزش گذاری (I - K)"
#: erpnext/stock/report/stock_balance/stock_balance.js:76
#: erpnext/stock/report/stock_ledger/stock_ledger.js:96
@@ -58646,11 +58646,11 @@ msgstr "عملیات دستور کار"
#. Label of the work_order_qty (Float) field in DocType 'Sales Order Item'
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
msgid "Work Order Qty"
-msgstr "تعداد دستور کار"
+msgstr "مقدار دستور کار"
#: erpnext/manufacturing/dashboard_fixtures.py:152
msgid "Work Order Qty Analysis"
-msgstr "تجزیه و تحلیل تعداد دستور کار"
+msgstr "تجزیه و تحلیل مقدار دستور کار"
#. Name of a report
#: erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.json
diff --git a/erpnext/locale/sv.po b/erpnext/locale/sv.po
index 1614ff713f49..3fdbcb8b4b46 100644
--- a/erpnext/locale/sv.po
+++ b/erpnext/locale/sv.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: info@erpnext.com\n"
"POT-Creation-Date: 2024-12-08 09:35+0000\n"
-"PO-Revision-Date: 2024-12-09 07:04\n"
+"PO-Revision-Date: 2024-12-12 07:06\n"
"Last-Translator: info@erpnext.com\n"
"Language-Team: Swedish\n"
"MIME-Version: 1.0\n"
@@ -6326,7 +6326,7 @@ msgstr "Automatiskt Skapa Inköp Följesedel"
#. in DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Auto Create Serial and Batch Bundle For Outward"
-msgstr "Automatiskt Skapa Serie Nummer och Parti Paket för Försäljning"
+msgstr "Automatiskt Skapa Serie Nummer och Parti Paket för Utleverans"
#. Label of the auto_create_subcontracting_order (Check) field in DocType
#. 'Buying Settings'
@@ -15146,7 +15146,7 @@ msgstr "Dra Av"
#. Deduction Certificate'
#: erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json
msgid "Deductee Details"
-msgstr "Avdragstagare Detaljer"
+msgstr "Avdragsberättigad Detaljer"
#. Label of the deductions_or_loss_section (Section Break) field in DocType
#. 'Payment Entry'
@@ -15409,7 +15409,7 @@ msgstr "Standard Artikel Producent"
#. Label of the default_letter_head (Link) field in DocType 'Company'
#: erpnext/setup/doctype/company/company.json
msgid "Default Letter Head"
-msgstr "Standard Sidhuvud"
+msgstr "Standard Brevhuvud"
#. Label of the default_manufacturer_part_no (Data) field in DocType 'Item'
#: erpnext/stock/doctype/item/item.json
@@ -18424,7 +18424,7 @@ msgstr "E-post"
#: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:49
msgid "Email Receipt"
-msgstr "E-posta Kvitto"
+msgstr "E-posta"
#. Label of the email_sent (Check) field in DocType 'Request for Quotation
#. Supplier'
@@ -19226,7 +19226,7 @@ msgstr "Utvärdering Period"
#. DocType 'Tax Withholding Category'
#: erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json
msgid "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach"
-msgstr "Även fakturor med tillämpa moms undanhållning omarkerad kommer att betraktas för att kontrollera kumulativ tröskel överträdelse"
+msgstr "Även fakturor med tillämpa momsavdrag omarkerad kommer att beaktas för att kontrollera kumulativ tröskel överträdelse"
#. Label of the event (Data) field in DocType 'Advance Payment Ledger Entry'
#: erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json
@@ -20588,7 +20588,7 @@ msgstr "Fast Tillgång Register"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:25
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:38
msgid "Fixed Assets"
-msgstr "Anläggningstillgångar"
+msgstr "Fasta Tillgångar"
#. Label of the fixed_deposit_number (Data) field in DocType 'Bank Guarantee'
#: erpnext/accounts/doctype/bank_guarantee/bank_guarantee.json
@@ -20605,7 +20605,7 @@ msgstr "Fixad Fel Logg"
#. 'Subscription Plan'
#: erpnext/accounts/doctype/subscription_plan/subscription_plan.json
msgid "Fixed Rate"
-msgstr "Fast Ränta"
+msgstr "Fast Pris"
#. Label of the fixed_time (Check) field in DocType 'BOM Operation'
#: erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -22094,7 +22094,7 @@ msgstr "Överförd"
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1738
msgid "Goods are already received against the outward entry {0}"
-msgstr "Redan mottagen mot utgående post {0}"
+msgstr "Artiklarna redan mottagna mot utleverans post {0}"
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:173
msgid "Government"
@@ -25219,7 +25219,7 @@ msgstr "Fakturering Funktioner"
#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
msgid "Inward"
-msgstr "Ingående"
+msgstr "Inleverans"
#. Label of the is_account_payable (Check) field in DocType 'Cheque Print
#. Template'
@@ -25582,7 +25582,7 @@ msgstr "Är Öppning Post"
#. Label of the is_outward (Check) field in DocType 'Serial and Batch Entry'
#: erpnext/stock/doctype/serial_and_batch_entry/serial_and_batch_entry.json
msgid "Is Outward"
-msgstr "Är Utgående"
+msgstr "Är Utleverans"
#. Label of the is_paid (Check) field in DocType 'Purchase Invoice'
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -28190,7 +28190,7 @@ msgstr "Lägre än Belopp"
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
msgid "Letter Head"
-msgstr "Sidhuvud"
+msgstr "Brevhuvud"
#. Description of the 'Body Text' (Text Editor) field in DocType 'Dunning
#. Letter Text'
@@ -31544,7 +31544,7 @@ msgstr "Ny Försäljning Faktura"
#. Label of the sales_order (Check) field in DocType 'Email Digest'
#: erpnext/setup/doctype/email_digest/email_digest.json
msgid "New Sales Orders"
-msgstr "Ny Försäljning Order"
+msgstr "Nya Försäljning Ordrar"
#: erpnext/setup/doctype/sales_person/sales_person_tree.js:3
msgid "New Sales Person Name"
@@ -33722,7 +33722,7 @@ msgstr "Utstående för {0} kan inte vara mindre än noll ({1})"
#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
msgid "Outward"
-msgstr "Utgående"
+msgstr "Utleverans"
#. Label of the over_billing_allowance (Currency) field in DocType 'Accounts
#. Settings'
@@ -38353,7 +38353,7 @@ msgstr "Utskrift Inställningar"
#: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:62
#: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:231
msgid "Print Receipt"
-msgstr "Skriv ut Faktura"
+msgstr "Skriv ut"
#. Label of the print_settings (Section Break) field in DocType 'Accounts
#. Settings'
@@ -49830,7 +49830,7 @@ msgstr "Lager Post"
#. Label of the outgoing_stock_entry (Link) field in DocType 'Stock Entry'
#: erpnext/stock/doctype/stock_entry/stock_entry.json
msgid "Stock Entry (Outward GIT)"
-msgstr "Lager Post (Utgående GIT)"
+msgstr "Lager Post (Utleverans GIT)"
#. Label of the ste_detail (Data) field in DocType 'Stock Entry Detail'
#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -53099,7 +53099,7 @@ msgstr "Serie Nummer på rad #{0}: {1} är inte tillgänglig i lager {2}."
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1415
msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}"
-msgstr "Serie och Parti Paket {0} är inte giltigt för denna transaktion. \"Typ av Transaktion\" ska vara \"Utgående\" istället för \"Ingående\" i Serie och Parti Paket {0}"
+msgstr "Serie och Parti Paket {0} är inte giltigt för denna transaktion. \"Typ av Transaktion\" ska vara \"Utleverans\" istället för \"Inleverans\" i Serie och Parti Paket {0}"
#: erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.js:17
msgid "The Stock Entry of type 'Manufacture' is known as backflush. Raw materials being consumed to manufacture finished goods is known as backflushing.
When creating Manufacture Entry, raw-material items are backflushed based on BOM of production item. If you want raw-material items to be backflushed based on Material Transfer entry made against that Work Order instead, then you can set it under this field."
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index 4c78d939ebcf..9598786821db 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -356,7 +356,7 @@ var calculate_end_time = function (frm, cdt, cdn) {
if (child.hours) {
d.add(child.hours, "hours");
frm._setting_hours = true;
- frappe.model.set_value(cdt, cdn, "to_time", d.format(frappe.defaultDatetimeFormat)).then(() => {
+ frappe.model.set_value(cdt, cdn, "to_time", frappe.datetime.get_datetime_as_string(d)).then(() => {
frm._setting_hours = false;
});
}
diff --git a/erpnext/public/images/v16/bom_browser.jpg b/erpnext/public/images/v16/bom_browser.jpg
new file mode 100644
index 000000000000..a7ccc10c2484
Binary files /dev/null and b/erpnext/public/images/v16/bom_browser.jpg differ
diff --git a/erpnext/public/images/v16/erpnext.svg b/erpnext/public/images/v16/erpnext.svg
new file mode 100644
index 000000000000..d699ea2ad2f1
--- /dev/null
+++ b/erpnext/public/images/v16/erpnext.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/erpnext/public/images/v16/p_l_graph.png b/erpnext/public/images/v16/p_l_graph.png
new file mode 100644
index 000000000000..0780634f4bb2
Binary files /dev/null and b/erpnext/public/images/v16/p_l_graph.png differ
diff --git a/erpnext/public/images/v16/tasks.png b/erpnext/public/images/v16/tasks.png
new file mode 100644
index 000000000000..c3c0a1a78bb3
Binary files /dev/null and b/erpnext/public/images/v16/tasks.png differ
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index a4f65a25f2ff..ea9aa95b1448 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -498,7 +498,29 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item_code(doc, cdt, cdn) {
var me = this;
+ // Experimental: This will be removed once stability is achieved.
+ if (frappe.boot.sysdefaults.use_server_side_reactivity) {
+ var item = frappe.get_doc(cdt, cdn);
+ frappe.call({
+ doc: doc,
+ method: "process_item_selection",
+ args: {
+ item: item.name
+ },
+ callback: function(r) {
+ if(!r.exc) {
+ me.frm.refresh_fields();
+ }
+ }
+ });
+ } else {
+ me.process_item_selection(doc, cdt, cdn);
+ }
+ }
+
+ process_item_selection(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
+ var me = this;
var update_stock = 0, show_batch_dialog = 0;
item.weight_per_unit = 0;
@@ -510,7 +532,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
show_batch_dialog = update_stock;
} else if((this.frm.doc.doctype === 'Purchase Receipt') ||
- this.frm.doc.doctype === 'Delivery Note') {
+ this.frm.doc.doctype === 'Delivery Note') {
show_batch_dialog = 1;
}
@@ -583,10 +605,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.run_serially([
() => {
if (item.docstatus === 0
- && frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
- && !item.use_serial_batch_fields
- && cint(frappe.user_defaults?.use_serial_batch_fields) === 1
- ) {
+ && frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
+ && !item.use_serial_batch_fields
+ && cint(frappe.user_defaults?.use_serial_batch_fields) === 1
+ ) {
item["use_serial_batch_fields"] = 1;
}
},
@@ -601,7 +623,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
// for internal customer instead of pricing rule directly apply valuation rate on item
if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && me.frm.doc.represents_company === me.frm.doc.company) {
me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time,
- me.frm.doc.doctype, me.frm.doc.company);
+ me.frm.doc.doctype, me.frm.doc.company);
} else {
me.frm.script_manager.trigger("price_list_rate", cdt, cdn);
}
@@ -615,24 +637,24 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
() => {
if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
- .then((r) => {
- if (r.message &&
- (r.message.has_batch_no || r.message.has_serial_no)) {
- frappe.flags.hide_serial_batch_dialog = false;
- } else {
- show_batch_dialog = false;
- }
- });
+ .then((r) => {
+ if (r.message &&
+ (r.message.has_batch_no || r.message.has_serial_no)) {
+ frappe.flags.hide_serial_batch_dialog = false;
+ } else {
+ show_batch_dialog = false;
+ }
+ });
},
() => {
// check if batch serial selector is disabled or not
if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog)
return frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector')
- .then((value) => {
- if (value) {
- frappe.flags.hide_serial_batch_dialog = true;
- }
- });
+ .then((value) => {
+ if (value) {
+ frappe.flags.hide_serial_batch_dialog = true;
+ }
+ });
},
() => {
if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
@@ -676,6 +698,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
+
price_list_rate(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
diff --git a/erpnext/public/js/projects/timer.js b/erpnext/public/js/projects/timer.js
index 10f062721893..73ff4ba09bdd 100644
--- a/erpnext/public/js/projects/timer.js
+++ b/erpnext/public/js/projects/timer.js
@@ -89,7 +89,7 @@ erpnext.timesheet.control_timer = function (frm, dialog, row, timestamp = 0) {
let d = moment(row.from_time);
if (row.expected_hours) {
d.add(row.expected_hours, "hours");
- row.to_time = d.format(frappe.defaultDatetimeFormat);
+ row.to_time = frappe.datetime.get_datetime_as_string(d);
}
frm.refresh_field("time_logs");
frm.save();
@@ -117,8 +117,7 @@ erpnext.timesheet.control_timer = function (frm, dialog, row, timestamp = 0) {
grid_row.doc.project = args.project;
grid_row.doc.task = args.task;
grid_row.doc.expected_hours = args.expected_hours;
- grid_row.doc.hours = currentIncrement / 3600;
- grid_row.doc.to_time = frappe.datetime.now_datetime();
+ grid_row.doc.to_time = frappe.datetime.get_datetime_as_string();
grid_row.refresh();
frm.dirty();
frm.save();
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index de230d710da7..6bafbaf29edf 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -456,6 +456,7 @@
"fieldtype": "Column Break"
},
{
+ "allow_on_submit": 1,
"fieldname": "projected_qty",
"fieldtype": "Float",
"label": "Projected Qty",
@@ -697,7 +698,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-11-24 14:18:43.952844",
+ "modified": "2024-12-12 13:49:17.765883",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index c873561df88a..8203abcf2e27 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -33,7 +33,9 @@
"dont_reserve_sales_order_qty_on_sales_return",
"hide_tax_id",
"enable_discount_accounting",
- "enable_cutoff_date_on_bulk_delivery_note_creation"
+ "enable_cutoff_date_on_bulk_delivery_note_creation",
+ "experimental_section",
+ "use_server_side_reactivity"
],
"fields": [
{
@@ -207,6 +209,17 @@
"fieldname": "enable_cutoff_date_on_bulk_delivery_note_creation",
"fieldtype": "Check",
"label": "Enable Cut-Off Date on Bulk Delivery Note Creation"
+ },
+ {
+ "fieldname": "experimental_section",
+ "fieldtype": "Section Break",
+ "label": "Experimental"
+ },
+ {
+ "default": "1",
+ "fieldname": "use_server_side_reactivity",
+ "fieldtype": "Check",
+ "label": "Use Server Side Reactivity"
}
],
"icon": "fa fa-cog",
@@ -214,7 +227,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-03-27 13:10:38.633352",
+ "modified": "2024-12-06 11:41:54.722337",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index a48817715733..216a74ab6889 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -40,6 +40,7 @@ class SellingSettings(Document):
selling_price_list: DF.Link | None
so_required: DF.Literal["No", "Yes"]
territory: DF.Link | None
+ use_server_side_reactivity: DF.Check
validate_selling_price: DF.Check
# end: auto-generated types
@@ -69,15 +70,15 @@ def validate(self):
)
def toggle_hide_tax_id(self):
- self.hide_tax_id = cint(self.hide_tax_id)
+ _hide_tax_id = cint(self.hide_tax_id)
# Make property setters to hide tax_id fields
for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"):
make_property_setter(
- doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False
+ doctype, "tax_id", "hidden", _hide_tax_id, "Check", validate_fields_for_doctype=False
)
make_property_setter(
- doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False
+ doctype, "tax_id", "print_hide", _hide_tax_id, "Check", validate_fields_for_doctype=False
)
def toggle_editable_rate_for_bundle_items(self):
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index 4a2d8911d1a8..ed6e6e02dccc 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -85,7 +85,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
${format_currency(doc.paid_amount, doc.currency)}
${doc.name}
-
${doc.status}
+
${__(doc.status)}
`;
}
diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json
index b10a99544afc..12de0d7501a3 100644
--- a/erpnext/selling/workspace/selling/selling.json
+++ b/erpnext/selling/workspace/selling/selling.json
@@ -1,4 +1,5 @@
{
+ "app": "erpnext",
"charts": [
{
"chart_name": "Sales Order Trends",
@@ -621,7 +622,7 @@
"type": "Link"
}
],
- "modified": "2024-07-18 04:16:18.176054",
+ "modified": "2024-12-13 14:37:39.781540",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling",
@@ -676,5 +677,6 @@
"type": "Dashboard"
}
],
- "title": "Selling"
-}
+ "title": "Selling",
+ "type": "Workspace"
+}
\ No newline at end of file
diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py
index 6ef0cdeee38a..12de9273834f 100644
--- a/erpnext/startup/boot.py
+++ b/erpnext/startup/boot.py
@@ -16,6 +16,9 @@ def boot_session(bootinfo):
bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory")
bootinfo.sysdefaults.customer_group = frappe.db.get_single_value("Selling Settings", "customer_group")
+ bootinfo.sysdefaults.use_server_side_reactivity = frappe.db.get_single_value(
+ "Selling Settings", "use_server_side_reactivity"
+ )
bootinfo.sysdefaults.allow_stale = cint(
frappe.db.get_single_value("Accounts Settings", "allow_stale")
)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 9bc96ded6ebb..2a8c1b6d073d 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -4033,6 +4033,69 @@ def test_do_not_allow_to_inward_same_serial_no_multiple_times(self):
frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1)
+ def test_seral_no_return_validation(self):
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ make_purchase_return,
+ )
+
+ sn_item_code = make_item(
+ "Test Serial No for Validation", {"has_serial_no": 1, "serial_no_series": "SN-TSNFVAL-.#####"}
+ ).name
+
+ pr1 = make_purchase_receipt(item_code=sn_item_code, qty=5, rate=100, use_serial_batch_fields=1)
+ pr1_serial_nos = get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle)
+
+ serial_no_pr = make_purchase_receipt(
+ item_code=sn_item_code, qty=5, rate=100, use_serial_batch_fields=1
+ )
+ serial_no_pr_serial_nos = get_serial_nos_from_bundle(serial_no_pr.items[0].serial_and_batch_bundle)
+
+ sn_return = make_purchase_return(serial_no_pr.name)
+ sn_return.items[0].qty = -1
+ sn_return.items[0].received_qty = -1
+ sn_return.items[0].serial_no = pr1_serial_nos[0]
+ sn_return.save()
+ self.assertRaises(frappe.ValidationError, sn_return.submit)
+
+ sn_return = make_purchase_return(serial_no_pr.name)
+ sn_return.items[0].qty = -1
+ sn_return.items[0].received_qty = -1
+ sn_return.items[0].serial_no = serial_no_pr_serial_nos[0]
+ sn_return.save()
+ sn_return.submit()
+
+ def test_batch_no_return_validation(self):
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
+ make_purchase_return,
+ )
+
+ batch_item_code = make_item(
+ "Test Batch No for Validation",
+ {"has_batch_no": 1, "batch_number_series": "BT-TSNFVAL-.#####", "create_new_batch": 1},
+ ).name
+
+ pr1 = make_purchase_receipt(item_code=batch_item_code, qty=5, rate=100, use_serial_batch_fields=1)
+ batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
+
+ batch_no_pr = make_purchase_receipt(
+ item_code=batch_item_code, qty=5, rate=100, use_serial_batch_fields=1
+ )
+ original_batch_no = get_batch_from_bundle(batch_no_pr.items[0].serial_and_batch_bundle)
+
+ batch_return = make_purchase_return(batch_no_pr.name)
+ batch_return.items[0].qty = -1
+ batch_return.items[0].received_qty = -1
+ batch_return.items[0].batch_no = batch_no
+ batch_return.save()
+ self.assertRaises(frappe.ValidationError, batch_return.submit)
+
+ batch_return = make_purchase_return(batch_no_pr.name)
+ batch_return.items[0].qty = -1
+ batch_return.items[0].received_qty = -1
+ batch_return.items[0].batch_no = original_batch_no
+ batch_return.save()
+ batch_return.submit()
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index a1f3135b70b8..dc2071b9ee07 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -252,8 +252,8 @@ def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_st
]:
return
- if return_aginst := self.get_return_aginst(parent=parent):
- self.set_valuation_rate_for_return_entry(return_aginst, save)
+ if return_against := self.get_return_against(parent=parent):
+ self.set_valuation_rate_for_return_entry(return_against, save)
elif self.type_of_transaction == "Outward":
self.set_incoming_rate_for_outward_transaction(
row, save, allow_negative_stock=allow_negative_stock
@@ -261,9 +261,12 @@ def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_st
else:
self.set_incoming_rate_for_inward_transaction(row, save)
- def set_valuation_rate_for_return_entry(self, return_aginst, save=False):
- if valuation_details := self.get_valuation_rate_for_return_entry(return_aginst):
+ def set_valuation_rate_for_return_entry(self, return_against, save=False):
+ if valuation_details := self.get_valuation_rate_for_return_entry(return_against):
for row in self.entries:
+ if valuation_details:
+ self.validate_returned_serial_batch_no(return_against, row, valuation_details)
+
if row.serial_no:
valuation_rate = valuation_details["serial_nos"].get(row.serial_no)
else:
@@ -280,7 +283,22 @@ def set_valuation_rate_for_return_entry(self, return_aginst, save=False):
}
)
- def get_valuation_rate_for_return_entry(self, return_aginst):
+ def validate_returned_serial_batch_no(self, return_against, row, original_inv_details):
+ if row.serial_no and row.serial_no not in original_inv_details["serial_nos"]:
+ self.throw_error_message(
+ _(
+ "Serial No {0} is not present in the {1} {2}, hence you can't return it against the {1} {2}"
+ ).format(bold(row.serial_no), self.voucher_type, bold(return_against))
+ )
+
+ if row.batch_no and row.batch_no not in original_inv_details["batches"]:
+ self.throw_error_message(
+ _(
+ "Batch No {0} is not present in the original {1} {2}, hence you can't return it against the {1} {2}"
+ ).format(bold(row.batch_no), self.voucher_type, bold(return_against))
+ )
+
+ def get_valuation_rate_for_return_entry(self, return_against):
valuation_details = frappe._dict(
{
"serial_nos": defaultdict(float),
@@ -296,7 +314,7 @@ def get_valuation_rate_for_return_entry(self, return_aginst):
"`tabSerial and Batch Entry`.`incoming_rate`",
],
filters=[
- ["Serial and Batch Bundle", "voucher_no", "=", return_aginst],
+ ["Serial and Batch Bundle", "voucher_no", "=", return_against],
["Serial and Batch Entry", "docstatus", "=", 1],
["Serial and Batch Bundle", "is_cancelled", "=", 0],
["Serial and Batch Bundle", "item_code", "=", self.item_code],
@@ -430,8 +448,8 @@ def get_sle_for_outward_transaction(self):
return sle
- def get_return_aginst(self, parent=None):
- return_aginst = None
+ def get_return_against(self, parent=None):
+ return_against = None
if parent and parent.get("is_return") and parent.get("return_against"):
return parent.get("return_against")
@@ -455,7 +473,7 @@ def get_return_aginst(self, parent=None):
if voucher_details and voucher_details.get("is_return") and voucher_details.get("return_against"):
return voucher_details.get("return_against")
- return return_aginst
+ return return_against
def set_incoming_rate_for_inward_transaction(self, row=None, save=False):
valuation_field = "valuation_rate"
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 319856780095..85acc7629692 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -207,6 +207,7 @@ frappe.ui.form.on("Stock Reconciliation", {
posting_time: frm.doc.posting_time,
batch_no: d.batch_no,
row: d,
+ company: frm.doc.company,
},
callback: function (r) {
const row = frappe.model.get_doc(cdt, cdn);
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index c13b3620517f..12773a5555db 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -466,6 +466,7 @@ def _changed(item):
batch_no=item.batch_no,
inventory_dimensions_dict=inventory_dimensions_dict,
row=item,
+ company=self.company,
)
if (
@@ -974,6 +975,7 @@ def recalculate_current_qty(self, voucher_detail_no):
self.posting_date,
self.posting_time,
row=row,
+ company=self.company,
)
current_qty = item_dict.get("qty")
@@ -1308,6 +1310,7 @@ def get_stock_balance_for(
with_valuation_rate: bool = True,
inventory_dimensions_dict=None,
row=None,
+ company=None,
):
frappe.has_permission("Stock Reconciliation", "write", throw=True)
@@ -1367,6 +1370,21 @@ def get_stock_balance_for(
or 0
)
+ if row.use_serial_batch_fields and row.batch_no:
+ rate = get_incoming_rate(
+ frappe._dict(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "qty": row.qty * -1,
+ "batch_no": row.batch_no,
+ "company": company,
+ "posting_date": posting_date,
+ "posting_time": posting_time,
+ }
+ )
+ )
+
return {
"qty": qty,
"rate": rate,
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
index 06479992283d..9b7d134847d6 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py
@@ -2,7 +2,7 @@
# For license information, please see license.txt
-from datetime import datetime
+from datetime import datetime, timezone
import frappe
from frappe import _
@@ -1007,13 +1007,13 @@ def now_datetime(user):
def convert_utc_to_user_timezone(utc_timestamp, user):
- from pytz import UnknownTimeZoneError, timezone
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
user_tz = get_tz(user)
- utcnow = timezone("UTC").localize(utc_timestamp)
+ utcnow = utc_timestamp.replace(tzinfo=timezone.utc)
try:
- return utcnow.astimezone(timezone(user_tz))
- except UnknownTimeZoneError:
+ return utcnow.astimezone(ZoneInfo(user_tz))
+ except ZoneInfoNotFoundError:
return utcnow
diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py
index bee00a84c3c8..d6af81e592c3 100644
--- a/erpnext/utilities/doctype/video/video.py
+++ b/erpnext/utilities/doctype/video/video.py
@@ -6,7 +6,6 @@
from datetime import datetime
import frappe
-import pytz
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
@@ -77,6 +76,8 @@ def get_frequency(value):
def update_youtube_data():
+ from zoneinfo import ZoneInfo
+
# Called every 30 minutes via hooks
video_settings = frappe.get_cached_doc("Video Settings")
if not video_settings.enable_youtube_tracking:
@@ -84,7 +85,7 @@ def update_youtube_data():
frequency = get_frequency(video_settings.frequency)
time = datetime.now()
- timezone = pytz.timezone(get_system_timezone())
+ timezone = ZoneInfo(get_system_timezone())
site_time = time.astimezone(timezone)
if frequency == 30:
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 2e4bdac6aab4..97b274da5765 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -7,7 +7,10 @@
from frappe import _
from frappe.utils import cint, flt, get_time, now_datetime
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.controllers.status_updater import StatusUpdater
+from erpnext.stock.get_item_details import get_item_details
+from erpnext.stock.utils import get_incoming_rate
class UOMMustBeIntegerError(frappe.ValidationError):
@@ -231,6 +234,259 @@ def validate_currency_for_receivable_payable_and_advance_account(self):
)
)
+ def fetch_item_details(self, item: dict) -> dict:
+ return get_item_details(
+ frappe._dict(
+ {
+ "item_code": item.get("item_code"),
+ "barcode": item.get("barcode"),
+ "serial_no": item.get("serial_no"),
+ "batch_no": item.get("batch_no"),
+ "set_warehouse": self.get("set_warehouse"),
+ "warehouse": item.get("warehouse"),
+ "customer": self.get("customer") or self.get("party_name"),
+ "quotation_to": self.get("quotation_to"),
+ "supplier": self.get("supplier"),
+ "currency": self.get("currency"),
+ "is_internal_supplier": self.get("is_internal_supplier"),
+ "is_internal_customer": self.get("is_internal_customer"),
+ "update_stock": self.update_stock
+ if self.doctype in ["Purchase Invoice", "Sales Invoice"]
+ else False,
+ "conversion_rate": self.get("conversion_rate"),
+ "price_list": self.get("selling_price_list") or self.get("buying_price_list"),
+ "price_list_currency": self.get("price_list_currency"),
+ "plc_conversion_rate": self.get("plc_conversion_rate"),
+ "company": self.get("company"),
+ "order_type": self.get("order_type"),
+ "is_pos": cint(self.get("is_pos")),
+ "is_return": cint(self.get("is_return)")),
+ "is_subcontracted": self.get("is_subcontracted"),
+ "ignore_pricing_rule": self.get("ignore_pricing_rule"),
+ "doctype": self.get("doctype"),
+ "name": self.get("name"),
+ "project": item.get("project") or self.get("project"),
+ "qty": item.get("qty") or 1,
+ "net_rate": item.get("rate"),
+ "base_net_rate": item.get("base_net_rate"),
+ "stock_qty": item.get("stock_qty"),
+ "conversion_factor": item.get("conversion_factor"),
+ "weight_per_unit": item.get("weight_per_unit"),
+ "uom": item.get("uom"),
+ "weight_uom": item.get("weight_uom"),
+ "manufacturer": item.get("manufacturer"),
+ "stock_uom": item.get("stock_uom"),
+ "pos_profile": self.get("pos_profile") if cint(self.get("is_pos")) else "",
+ "cost_center": item.get("cost_center"),
+ "tax_category": self.get("tax_category"),
+ "item_tax_template": item.get("item_tax_template"),
+ "child_doctype": item.get("doctype"),
+ "child_docname": item.get("name"),
+ "is_old_subcontracting_flow": self.get("is_old_subcontracting_flow"),
+ }
+ )
+ )
+
+ @frappe.whitelist()
+ def process_item_selection(self, item):
+ # Server side 'item' doc. Update this to reflect in UI
+ item_obj = self.get("items", {"name": item})[0]
+
+ # 'item_details' has latest item related values
+ item_details = self.fetch_item_details(item_obj)
+
+ self.set_fetched_values(item_obj, item_details)
+ self.set_item_rate_and_discounts(item_obj, item_details)
+ self.add_taxes_from_item_template(item_obj, item_details)
+ self.add_free_item(item_obj, item_details)
+ self.handle_internal_parties(item_obj, item_details)
+ self.conversion_factor(item_obj, item_details)
+ self.calculate_taxes_and_totals()
+
+ def set_fetched_values(self, item_obj: object, item_details: dict) -> None:
+ for k, v in item_details.items():
+ if hasattr(item_obj, k):
+ setattr(item_obj, k, v)
+
+ def handle_internal_parties(self, item_obj: object, item_details: dict) -> None:
+ if (
+ self.get("is_internal_customer") or self.get("is_internal_supplier")
+ ) and self.represents_company == self.company:
+ args = frappe._dict(
+ {
+ "item_code": item_obj.item_code,
+ "warehouse": item_obj.from_warehouse
+ if self.doctype in ["Purchase Receipt", "Purchase Invoice"]
+ else item_obj.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": item_obj.qty * item_obj.conversion_factor,
+ "serial_no": item_obj.serial_no,
+ "batch_no": item_obj.batch_no,
+ "voucher_type": self.doctype,
+ "company": self.company,
+ "allow_zero_valuation_rate": item_obj.allow_zero_valuation_rate,
+ }
+ )
+ rate = get_incoming_rate(args=args)
+ item_obj.rate = rate * item_obj.conversion_factor
+ else:
+ self.set_rate_based_on_price_list(item_obj, item_details)
+
+ def add_taxes_from_item_template(self, item_obj: object, item_details: dict) -> None:
+ if item_details.item_tax_rate and frappe.db.get_single_value(
+ "Accounts Settings", "add_taxes_from_item_tax_template"
+ ):
+ item_tax_template = frappe.json.loads(item_details.item_tax_rate)
+ for tax_head, _rate in item_tax_template.items():
+ found = [x for x in self.taxes if x.account_head == tax_head]
+ if not found:
+ self.append("taxes", {"charge_type": "On Net Total", "account_head": tax_head, "rate": 0})
+
+ def set_rate_based_on_price_list(self, item_obj: object, item_details: dict) -> None:
+ if item_obj.price_list_rate and item_obj.discount_percentage:
+ item_obj.rate = flt(
+ item_obj.price_list_rate * (1 - item_obj.discount_percentage / 100.0),
+ item_obj.precision("rate"),
+ )
+
+ def copy_from_first_row(self, row, fields):
+ if self.items and row:
+ fields.extend([x.get("fieldname") for x in get_dimensions(True)[0]])
+ first_row = self.items[0]
+ [setattr(row, k, first_row.get(k)) for k in fields if hasattr(first_row, k)]
+
+ def add_free_item(self, item_obj: object, item_details: dict) -> None:
+ free_items = item_details.get("free_item_data")
+ if free_items and len(free_items):
+ existing_free_items = [x for x in self.items if x.is_free_item]
+ for free_item in free_items:
+ _matches = [
+ x
+ for x in existing_free_items
+ if x.item_code == free_item.get("item_code")
+ and x.pricing_rules == free_item.get("pricing_rules")
+ ]
+ if _matches:
+ row_to_modify = _matches[0]
+ else:
+ row_to_modify = self.append("items")
+
+ for k, _v in free_item.items():
+ setattr(row_to_modify, k, free_item.get(k))
+
+ self.copy_from_first_row(row_to_modify, ["expense_account", "income_account"])
+
+ def conversion_factor(self, item_obj: object, item_details: dict) -> None:
+ if frappe.get_meta(item_obj.doctype).has_field("stock_qty"):
+ item_obj.stock_qty = flt(
+ item_obj.qty * item_obj.conversion_factor, item_obj.precision("stock_qty")
+ )
+
+ if self.doctype != "Material Request":
+ item_obj.total_weight = flt(item_obj.stock_qty * item_obj.weight_per_unit)
+ self.calculate_net_weight()
+
+ # TODO: for handling customization not to fetch price list rate
+ if frappe.flags.dont_fetch_price_list_rate:
+ return
+
+ if not frappe.flags.dont_fetch_price_list_rate and frappe.get_meta(self.doctype).has_field(
+ "price_list_currency"
+ ):
+ self._apply_price_list(item_obj, True)
+ self.calculate_stock_uom_rate(item_obj)
+
+ def calculate_stock_uom_rate(self, item_obj: object) -> None:
+ if item_obj.rate:
+ item_obj.stock_uom_rate = flt(item_obj.rate) / flt(item_obj.conversion_factor)
+
+ def set_item_rate_and_discounts(self, item_obj: object, item_details: dict) -> None:
+ effective_item_rate = item_details.price_list_rate
+ item_rate = item_details.rate
+
+ # Field order precedance
+ # blanket_order_rate -> margin_type -> discount_percentage -> discount_amount
+ if item_obj.parenttype in ["Sales Order", "Quotation"] and item_obj.blanket_order_rate:
+ effective_item_rate = item_obj.blanket_order_rate
+
+ if item_obj.margin_type == "Percentage":
+ item_obj.rate_with_margin = flt(effective_item_rate) + flt(effective_item_rate) * (
+ flt(item_obj.margin_rate_or_amount) / 100
+ )
+ else:
+ item_obj.rate_with_margin = flt(effective_item_rate) + flt(item_obj.margin_rate_or_amount)
+
+ item_obj.base_rate_with_margin = flt(item_obj.rate_with_margin) * flt(self.conversion_rate)
+ item_rate = flt(item_obj.rate_with_margin, item_obj.precision("rate"))
+
+ if item_obj.discount_percentage and not item_obj.discount_amount:
+ item_obj.discount_amount = (
+ flt(item_obj.rate_with_margin) * flt(item_obj.discount_percentage) / 100
+ )
+
+ if item_obj.discount_amount and item_obj.discount_amount > 0:
+ item_rate = flt(
+ (item_obj.rate_with_margin) - (item_obj.discount_amount), item_obj.precision("rate")
+ )
+ item_obj.discount_percentage = (
+ 100 * flt(item_obj.discount_amount) / flt(item_obj.rate_with_margin)
+ )
+
+ item_obj.rate = item_rate
+
+ def calculate_net_weight(self):
+ self.total_net_weight = sum([x.get("total_weight") or 0 for x in self.items])
+ self.apply_shipping_rule()
+
+ def _apply_price_list(self, item_obj: object, reset_plc_conversion: bool) -> None:
+ if self.doctype == "Material Request":
+ return
+
+ if not reset_plc_conversion:
+ self.plc_conversion_rate = ""
+
+ if not self.items or not (item_obj.get("selling_price_list") or item_obj.get("buying_price_list")):
+ return
+
+ if self.get("in_apply_price_list"):
+ return
+
+ self.in_apply_price_list = True
+
+ from erpnext.stock.get_item_details import apply_price_list
+
+ args = {
+ "items": [x.as_dict() for x in self.items],
+ "customer": self.customer or self.party_name,
+ "quotation_to": self.quotation_to,
+ "customer_group": self.customer_group,
+ "territory": self.territory,
+ "supplier": self.supplier,
+ "supplier_group": self.supplier_group,
+ "currency": self.currency,
+ "conversion_rate": self.conversion_rate,
+ "price_list": self.selling_price_list or self.buying_price_list,
+ "price_list_currency": self.price_list_currency,
+ "plc_conversion_rate": self.plc_conversion_rate,
+ "company": self.company,
+ "transaction_date": self.transaction_date or self.posting_date,
+ "campaign": self.campaign,
+ "sales_partner": self.sales_partner,
+ "ignore_pricing_rule": self.ignore_pricing_rule,
+ "doctype": self.doctype,
+ "name": self.name,
+ "is_return": self.is_return,
+ "update_stock": self.update_stock if self.doctype in ["Sales Invoice", "Purchase Invoice"] else 0,
+ "conversion_factor": self.conversion_factor,
+ "pos_profile": self.pos_profile if self.doctype == "Sales Invoice" else "",
+ "coupon_code": self.coupon_code,
+ "is_internal_supplier": self.is_internal_supplier,
+ "is_internal_customer": self.is_internal_customer,
+ }
+ # TODO: test method call impact on document
+ apply_price_list(cts=args, as_doc=True, doc=self)
+
def delete_events(ref_type, ref_name):
events = (
diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py
index f50c207ab98e..fe75aea33cf8 100644
--- a/erpnext/www/book_appointment/index.py
+++ b/erpnext/www/book_appointment/index.py
@@ -1,8 +1,8 @@
import datetime
import json
+import zoneinfo
import frappe
-import pytz
from frappe import _
from frappe.utils.data import get_system_timezone
@@ -38,9 +38,7 @@ def get_appointment_settings():
@frappe.whitelist(allow_guest=True)
def get_timezones():
- import pytz
-
- return pytz.all_timezones
+ return zoneinfo.available_timezones()
@frappe.whitelist(allow_guest=True)
@@ -125,17 +123,17 @@ def filter_timeslots(date, timeslots):
def convert_to_guest_timezone(guest_tz, datetimeobject):
- guest_tz = pytz.timezone(guest_tz)
- local_timezone = pytz.timezone(get_system_timezone())
+ guest_tz = zoneinfo.ZoneInfo(guest_tz)
+ local_timezone = zoneinfo.ZoneInfo(get_system_timezone())
datetimeobject = local_timezone.localize(datetimeobject)
datetimeobject = datetimeobject.astimezone(guest_tz)
return datetimeobject
def convert_to_system_timezone(guest_tz, datetimeobject):
- guest_tz = pytz.timezone(guest_tz)
+ guest_tz = zoneinfo.ZoneInfo.timezone(guest_tz)
datetimeobject = guest_tz.localize(datetimeobject)
- system_tz = pytz.timezone(get_system_timezone())
+ system_tz = zoneinfo.ZoneInfo.timezone(get_system_timezone())
datetimeobject = datetimeobject.astimezone(system_tz)
return datetimeobject