diff --git a/README.md b/README.md index 4f65ceb70bd6..953f6c52e2be 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,75 @@
- + ERPNext Logo

ERPNext

-

ERP made simple

+

Powerful, Intuitive and Open-Source ERP

[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml/badge.svg?event=schedule)](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml) -[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext) -[![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext) [![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker) -[https://erpnext.com](https://erpnext.com) +
+
+
+Screenshots + Profit and Loss + BOM Browser + Tasks
+ -ERPNext as a monolith includes the following areas for managing businesses: - -1. [Accounting](https://erpnext.com/open-source-accounting) -1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system) -1. [CRM](https://erpnext.com/open-source-crm) -1. [Sales](https://erpnext.com/open-source-sales-purchase) -1. [Purchase](https://erpnext.com/open-source-sales-purchase) -1. [HRMS](https://erpnext.com/open-source-hrms) -1. [Project Management](https://erpnext.com/open-source-projects) -1. [Support](https://erpnext.com/open-source-help-desk-software) -1. [Asset Management](https://erpnext.com/open-source-asset-management-software) -1. [Quality Management](https://erpnext.com/docs/user/manual/en/quality-management) -1. [Manufacturing](https://erpnext.com/open-source-manufacturing-erp-software) -1. [Website Management](https://erpnext.com/open-source-website-builder-software) -1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext) -1. [And More](https://erpnext.com/docs/user/manual/en/) - -ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript. - -## Installation - -
- - - - - Try in PWD - +
+ Live Demo + - + Website + - + Documentation +
+ +# 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. + +
+ + + + Try on Frappe Cloud + +
-> 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). + +
+
+
+ + + + Frappe Technologies + + +
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 {
${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