diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json index 544f4fd6640..8ea57191024 100644 --- a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json +++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json @@ -16,6 +16,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "section_break_8", "rate", "section_break_9", @@ -92,6 +93,13 @@ "fieldname": "dimension_col_break", "fieldtype": "Column Break" }, + { + "allow_on_submit": 1, + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "fieldname": "section_break_8", "fieldtype": "Section Break" diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 6d1040a02ad..4ff2a13eae2 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -137,8 +137,10 @@ def get_payment_entries_for_bank_clearance( entries = [] condition = "" + pe_condition = "" if not include_reconciled_entries: condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')" + pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')" journal_entries = frappe.db.sql( f""" @@ -163,19 +165,20 @@ def get_payment_entries_for_bank_clearance( payment_entries = frappe.db.sql( f""" select - "Payment Entry" as payment_document, name as payment_entry, - reference_no as cheque_number, reference_date as cheque_date, - if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit, - if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit, - posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date, - if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency - from `tabPayment Entry` + "Payment Entry" as payment_document, pe.name as payment_entry, + pe.reference_no as cheque_number, pe.reference_date as cheque_date, + if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit, + if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit, + pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date, + if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency + from `tabPayment Entry` as pe + join `tabCompany` c on c.name = pe.company where - (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1 - and posting_date >= %(from)s and posting_date <= %(to)s - {condition} + (pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1 + and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s + {pe_condition} order by - posting_date ASC, name DESC + pe.posting_date ASC, pe.name DESC """, { "account": account, diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 37ac27f3722..46690a1a29a 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -131,18 +131,20 @@ class GLEntry(Document): if not self.is_cancelled and not (self.party_type and self.party): account_type = frappe.get_cached_value("Account", self.account, "account_type") - if account_type == "Receivable": - frappe.throw( - _("{0} {1}: Customer is required against Receivable account {2}").format( - self.voucher_type, self.voucher_no, self.account + # skipping validation for payroll entry creation in case party is not required + if not frappe.flags.party_not_required_for_receivable_payable: + if account_type == "Receivable": + frappe.throw( + _("{0} {1}: Customer is required against Receivable account {2}").format( + self.voucher_type, self.voucher_no, self.account + ) ) - ) - elif account_type == "Payable": - frappe.throw( - _("{0} {1}: Supplier is required against Payable account {2}").format( - self.voucher_type, self.voucher_no, self.account + elif account_type == "Payable": + frappe.throw( + _("{0} {1}: Supplier is required against Payable account {2}").format( + self.voucher_type, self.voucher_no, self.account + ) ) - ) # Zero value transaction is not allowed if not ( diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index a54bae9fb96..7f71454d11d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -542,8 +542,11 @@ class JournalEntry(AccountsController): def validate_party(self): for d in self.get("accounts"): account_type = frappe.get_cached_value("Account", d.account, "account_type") + + # skipping validation for payroll entry creation + skip_validation = frappe.flags.party_not_required_for_receivable_payable if account_type in ["Receivable", "Payable"]: - if not (d.party_type and d.party): + if not (d.party_type and d.party) and not skip_validation: frappe.throw( _( "Row {0}: Party Type and Party is required for Receivable / Payable account {1}" diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.json b/erpnext/accounts/doctype/loyalty_program/loyalty_program.json index f4eb500b1b6..6a0c7d4ab70 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.json +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.json @@ -24,6 +24,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "help_section", "loyalty_program_help" ], @@ -143,6 +144,12 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "modified": "2019-05-26 09:11:46.120251", diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json index daee8f8c1ab..5a43e1dbf3d 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json @@ -13,6 +13,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "section_break_4", "invoices" ], @@ -62,6 +63,12 @@ "label": "Cost Center", "options": "Cost Center" }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 219d6089a57..a0e9183f7ba 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -28,6 +28,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "sec_break1", "invoice_name", "invoices", @@ -193,6 +194,12 @@ "label": "Cost Center", "options": "Cost Center" }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "depends_on": "eval:doc.party", "description": "Only 'Payment Entries' made against this advance account are supported.", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index fe2b806a76b..1e1a71ea707 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -5,6 +5,7 @@ import frappe from frappe import _, msgprint, qb from frappe.model.document import Document +from frappe.model.meta import get_field_precision from frappe.query_builder import Criterion from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today @@ -392,6 +393,12 @@ class PaymentReconciliation(Document): inv.outstanding_amount = flt(entry.get("outstanding_amount")) def get_difference_amount(self, payment_entry, invoice, allocated_amount): + allocated_amount_precision = get_field_precision( + frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount") + ) + difference_amount_precision = get_field_precision( + frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount") + ) difference_amount = 0 if frappe.get_cached_value( "Account", self.receivable_payable_account, "account_currency" @@ -399,8 +406,14 @@ class PaymentReconciliation(Document): if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( "exchange_rate", 1 ): - allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount - allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount + allocated_amount_in_ref_rate = flt( + payment_entry.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision), + difference_amount_precision, + ) + allocated_amount_in_inv_rate = flt( + invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision), + difference_amount_precision, + ) difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate return difference_amount diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 008d63ae76f..ea26d2b5460 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -22,6 +22,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "section_break_9", "account_currency", "tax_amount", @@ -211,6 +212,13 @@ "fieldname": "dimension_col_break", "fieldtype": "Column Break" }, + { + "allow_on_submit": 1, + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "default": "0", "depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 9e0a7983b74..8f7b1ece3c7 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -16,6 +16,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "section_break_8", "rate", "section_break_9", @@ -188,6 +189,13 @@ "fieldname": "dimension_col_break", "fieldtype": "Column Break" }, + { + "allow_on_submit": 1, + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "default": "0", "depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.json b/erpnext/accounts/doctype/shipping_rule/shipping_rule.json index 1b71db69eb5..8277c92d829 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.json +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.json @@ -16,6 +16,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "shipping_amount_section", "calculate_based_on", "column_break_8", @@ -136,6 +137,12 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "icon": "fa fa-truck", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index 680ccf69ac6..0109b957c1e 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -164,6 +164,12 @@ {% } %} +
+ {% if subtitle %} + {{ subtitle }} +
+ {% endif %} +
{% for(var i=0, l=data.length; i diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 6061be9f3a8..10a14ca4714 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -974,6 +974,7 @@ class ReceivablePayableReport: if self.account_type == "Receivable": self.add_customer_filters() + self.exclude_employee_transaction() elif self.account_type == "Payable": self.add_supplier_filters() @@ -1053,6 +1054,9 @@ class ReceivablePayableReport: ) ) + def exclude_employee_transaction(self): + self.qb_selection_filter.append(self.ple.party_type != "Employee") + def add_supplier_filters(self): supplier = qb.DocType("Supplier") if self.filters.get("supplier_group"): diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 04625fc76fc..040ac6008c0 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -38,6 +38,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement get_report_summary as get_pl_summary, ) from erpnext.accounts.report.utils import convert, convert_to_presentation_currency +from erpnext.accounts.utils import get_zero_cutoff def execute(filters=None): @@ -563,7 +564,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com row[company] = flt(d.get(company, 0.0), 3) - if abs(row[company]) >= 0.005: + if abs(row[company]) >= get_zero_cutoff(filters.presentation_currency): # ignore zero values has_value = True total += flt(row[company]) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index ed30ad415d0..2d0015a461e 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import ( filter_out_zero_value_rows, ) from erpnext.accounts.report.trial_balance.trial_balance import validate_filters +from erpnext.accounts.utils import get_zero_cutoff def execute(filters=None): @@ -154,7 +155,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list): for dimension in dimension_list: row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3) - if abs(row[frappe.scrub(dimension)]) >= 0.005: + if abs(row[frappe.scrub(dimension)]) >= get_zero_cutoff(company_currency): # ignore zero values has_value = True total += flt(d.get(frappe.scrub(dimension), 0.0), 3) diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html index 45d56a92037..f78775ec2ad 100644 --- a/erpnext/accounts/report/financial_statements.html +++ b/erpnext/accounts/report/financial_statements.html @@ -34,6 +34,12 @@ {% } %}
+
+ {% if subtitle %} + {{ subtitle }} +
+ {% endif %} +
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 04068dc08a1..bf46c9b07c6 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimension_with_children, ) from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year, get_zero_cutoff def get_period_list( @@ -304,7 +304,7 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum row[period.key] = flt(d.get(period.key, 0.0), 3) - if abs(row[period.key]) >= 0.005: + if abs(row[period.key]) >= get_zero_cutoff(company_currency): # ignore zero values has_value = True total += flt(row[period.key]) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index c40c607fbed..681636b1249 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -21,6 +21,12 @@ {%= frappe.datetime.str_to_user(filters.to_date) %}
+
+ {% if subtitle %} + {{ subtitle }} +
+ {% endif %} +
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 94331ef1b92..7c6c809b939 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -645,7 +645,7 @@ def get_columns(filters): "options": "GL Entry", "hidden": 1, }, - {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, { "label": _("Account"), "fieldname": "account", diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index dfb941d9123..5f3215fe7e2 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import ( filter_out_zero_value_rows, ) from erpnext.accounts.report.trial_balance.trial_balance import validate_filters +from erpnext.accounts.utils import get_zero_cutoff value_fields = ("income", "expense", "gross_profit_loss") @@ -149,7 +150,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on): for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) - if abs(row[key]) >= 0.005: + if abs(row[key]) >= get_zero_cutoff(company_currency): # ignore zero values has_value = True diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 5575426dfff..9e1dd1a57c3 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -18,6 +18,7 @@ from erpnext.accounts.report.financial_statements import ( set_gl_entries_by_account, ) from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency +from erpnext.accounts.utils import get_zero_cutoff value_fields = ( "opening_debit", @@ -413,7 +414,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) - if abs(row[key]) >= 0.005: + if abs(row[key]) >= get_zero_cutoff(company_currency): # ignore zero values has_value = True diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 5e108dee9b5..f89b47b2dfe 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -9,6 +9,7 @@ from erpnext.accounts.party import get_party_shipping_address from erpnext.accounts.utils import ( get_future_stock_vouchers, get_voucherwise_gl_entries, + get_zero_cutoff, sort_stock_vouchers_by_posting_date, ) from erpnext.stock.doctype.item.test_item import make_item @@ -156,6 +157,11 @@ class TestUtils(unittest.TestCase): self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year)) frappe.db.set_default("supp_master_name", "Supplier Name") + def test_get_zero_cutoff(self): + self.assertEqual(get_zero_cutoff(None), 0.005) + self.assertEqual(get_zero_cutoff("EUR"), 0.005) + self.assertEqual(get_zero_cutoff("BHD"), 0.0005) + ADDRESS_RECORDS = [ { diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 15117b2aa43..18cbc65731c 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -27,6 +27,7 @@ from frappe.utils import ( now, nowdate, ) +from frappe.utils.caching import site_cache from pypika import Order from pypika.functions import Coalesce from pypika.terms import ExistsCriterion @@ -1130,6 +1131,29 @@ def get_currency_precision(): return precision +def get_fraction_units(currency: str) -> int: + """Returns the number of fraction units for a currency.""" + fraction_units = frappe.db.get_value("Currency", currency, "fraction_units") + + if fraction_units is None: + fraction_units = 100 + + return fraction_units + + +@site_cache() +def get_zero_cutoff(currency: str) -> float: + """Returns the zero cutoff for a currency. + + For example, if the Fraction Units for a currency are set to 100, then the zero cutoff is 0.005. + We don't want to display values less than the zero cutoff. + This value was chosen for compatibility with the previous hard-coded value of 0.005. + """ + fraction_units = get_fraction_units(currency) + + return 0.5 / (fraction_units or 1) + + def get_held_invoices(party_type, party): """ Returns a list of names Purchase Invoices for the given party that are on hold @@ -2451,6 +2475,10 @@ def build_qb_match_conditions(doctype, user=None) -> list: for filter in match_filters: for link_option, allowed_values in filter.items(): fieldnames = link_fields_map.get(link_option, []) + cond = None + + if link_option == doctype: + cond = _dt["name"].isin(allowed_values) for fieldname in fieldnames: field = _dt[fieldname] @@ -2459,6 +2487,7 @@ def build_qb_match_conditions(doctype, user=None) -> list: if not apply_strict_user_permissions: cond = (Coalesce(field, "") == "") | cond + if cond: criterion.append(cond) return criterion diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index a6796e55f82..c2de548b2de 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -44,6 +44,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "target_fixed_asset_account" ], "fields": [ @@ -288,6 +289,12 @@ "label": "Cost Center", "options": "Cost Center" }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "fieldname": "dimension_col_break", "fieldtype": "Column Break" diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json index ebaaffbad15..98275acaef4 100644 --- a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json @@ -18,6 +18,7 @@ "accounting_dimensions_section", "cost_center", "dimension_col_break", + "project", "fixed_asset_account" ], "fields": [ @@ -98,6 +99,13 @@ "fieldname": "dimension_col_break", "fieldtype": "Column Break" }, + { + "allow_on_submit": 1, + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { "fieldname": "finance_book", "fieldtype": "Link", diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json index a6e67b956cf..d37c58bb5d6 100644 --- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -118,7 +118,8 @@ "fieldname": "rate", "fieldtype": "Currency", "in_list_view": 1, - "label": "Rate" + "label": "Rate", + "options": "currency" }, { "columns": 1, @@ -161,7 +162,8 @@ "fieldname": "amount", "fieldtype": "Currency", "label": "Amount", - "read_only": 1 + "read_only": 1, + "options": "currency" }, { "fieldname": "column_break_yuca", @@ -183,13 +185,15 @@ "fieldname": "base_amount", "fieldtype": "Currency", "hidden": 1, - "label": "Base Amount" + "label": "Base Amount", + "options": "Company:company:default_currency" }, { "fieldname": "base_rate", "fieldtype": "Currency", "hidden": 1, - "label": "Base Rate" + "label": "Base Rate", + "options": "Company:company:default_currency" }, { "default": "0", diff --git a/erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py b/erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py index 8f83898a877..21efdcfe936 100644 --- a/erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py +++ b/erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py @@ -6,7 +6,10 @@ def execute(): "POS Invoice Merge Log", {"docstatus": 1}, ["name", "pos_closing_entry"] ) + frappe.db.auto_commit_on_many_writes = 1 for log in pos_invoice_merge_logs: if log.pos_closing_entry and frappe.db.exists("POS Closing Entry", log.pos_closing_entry): company = frappe.db.get_value("POS Closing Entry", log.pos_closing_entry, "company") frappe.db.set_value("POS Invoice Merge Log", log.name, "company", company) + + frappe.db.auto_commit_on_many_writes = 0 diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index e3680549cc0..ed7e4bf623b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -931,7 +931,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe party_name = me.frm.doc.party_name } else{ - party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier"; + party_type = frappe.meta.has_field(me.frm.doc.doctype, "supplier") ? "Supplier" : "Customer"; party_name = me.frm.doc[party_type.toLowerCase()]; } if (party_name) { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 0118e89cd3c..eb6134d4259 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1762,6 +1762,11 @@ def create_pick_list(source_name, target_doc=None): target.qty = qty_to_be_picked target.stock_qty = qty_to_be_picked * flt(source.conversion_factor) + # update available qty + bin_details = get_bin_details(source.item_code, source.warehouse, source_parent.company) + target.actual_qty = bin_details.get("actual_qty") + target.company_total_stock = bin_details.get("company_total_stock") + def update_packed_item_qty(source, target, source_parent) -> None: qty = flt(source.qty) for item in source_parent.items: diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index 9b706836269..1e9a8278ec2 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -5,8 +5,10 @@ import unittest import frappe import frappe.utils +from frappe.query_builder import Criterion import erpnext +from erpnext.accounts.utils import build_qb_match_conditions from erpnext.setup.doctype.employee.employee import InactiveEmployeeStatusError test_records = frappe.get_test_records("Employee") @@ -34,6 +36,32 @@ class TestEmployee(unittest.TestCase): employee_doc.save() self.assertTrue("Employee" not in frappe.get_roles(user)) + def test_employee_user_permission(self): + employee1 = make_employee("employee_1_test@company.com", create_user_permission=1) + employee2 = make_employee("employee_2_test@company.com", create_user_permission=1) + make_employee("employee_3_test@company.com", create_user_permission=1) + + employee1_doc = frappe.get_doc("Employee", employee1) + employee2_doc = frappe.get_doc("Employee", employee2) + + employee2_doc.reload() + employee2_doc.reports_to = employee1_doc.name + employee2_doc.save() + + frappe.set_user(employee1_doc.user_id) + + Employee = frappe.qb.DocType("Employee") + qb_employee_list = ( + frappe.qb.from_(Employee) + .select(Employee.name) + .where(Criterion.all(build_qb_match_conditions("Employee"))) + .orderby(Employee.Name) + ).run(pluck=Employee.name) + employee_list = frappe.db.get_list("Employee", pluck="name", order_by="name") + + self.assertEqual(qb_employee_list, employee_list) + frappe.set_user("Administrator") + def tearDown(self): frappe.db.rollback() diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index c7d9823d144..7b87d75cee9 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -218,6 +218,7 @@ def get_batch_qty( batch_no=None, warehouse=None, item_code=None, + creation=None, posting_date=None, posting_time=None, ignore_voucher_nos=None, @@ -244,6 +245,7 @@ def get_batch_qty( { "item_code": item_code, "warehouse": warehouse, + "creation": creation, "posting_date": posting_date, "posting_time": posting_time, "batch_no": batch_no, diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 0ea35ddc1fb..e71c8eb6a00 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -724,7 +724,10 @@ class Item(Document): item_defaults = frappe.db.get_values( "Item Default", - {"parent": self.item_group}, + { + "parent": self.item_group, + "parenttype": "Item Group", + }, [ "company", "default_warehouse", diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index d021973a01f..9ea7f006df8 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -354,10 +354,12 @@ frappe.ui.form.on("Pick List Item", { item_code: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); if (row.item_code) { - get_item_details(row.item_code).then((data) => { + get_item_details(row.item_code, row.uom, row.warehouse, frm.doc.company).then((data) => { frappe.model.set_value(cdt, cdn, "uom", data.stock_uom); frappe.model.set_value(cdt, cdn, "stock_uom", data.stock_uom); frappe.model.set_value(cdt, cdn, "conversion_factor", 1); + frappe.model.set_value(cdt, cdn, "actual_qty", data.actual_qty); + frappe.model.set_value(cdt, cdn, "company_total_stock", data.company_total_stock); }); } }, @@ -371,6 +373,15 @@ frappe.ui.form.on("Pick List Item", { } }, + warehouse: (frm, cdt, cdn) => { + const row = frappe.get_doc(cdt, cdn); + if (!row.item_code || !row.warehouse) return; + get_item_details(row.item_code, row.uom, row.warehouse, frm.doc.company).then((data) => { + frappe.model.set_value(cdt, cdn, "actual_qty", data.actual_qty); + frappe.model.set_value(cdt, cdn, "company_total_stock", data.company_total_stock); + }); + }, + qty: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); frappe.model.set_value(cdt, cdn, "stock_qty", row.qty * row.conversion_factor); @@ -412,11 +423,13 @@ frappe.ui.form.on("Pick List Item", { }, }); -function get_item_details(item_code, uom = null) { +function get_item_details(item_code, uom = null, warehouse = null, company = null) { if (item_code) { return frappe.xcall("erpnext.stock.doctype.pick_list.pick_list.get_item_details", { item_code, uom, + warehouse, + company, }); } } diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 9417751f682..f49a4c955cc 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -21,7 +21,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor get_auto_batch_nos, get_picked_serial_nos, ) -from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.get_item_details import get_company_total_stock, get_conversion_factor from erpnext.stock.serial_batch_bundle import ( SerialBatchCreation, get_batches_from_bundle, @@ -74,6 +74,9 @@ class PickList(TransactionBase): if self.has_reserved_stock(): self.set_onload("has_reserved_stock", True) + for item in self.get("locations"): + item.update(get_item_details(item.item_code, item.uom, item.warehouse, self.company)) + def validate(self): self.validate_expired_batches() self.validate_for_qty() @@ -1442,15 +1445,29 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte @frappe.whitelist() -def get_item_details(item_code, uom=None): +def get_item_details(item_code, uom=None, warehouse=None, company=None): details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1) details.uom = uom or details.stock_uom if uom: details.update(get_conversion_factor(item_code, uom)) + if warehouse: + details.actual_qty = flt(get_actual_qty(item_code, warehouse)) + + if company: + details.company_total_stock = get_company_total_stock(item_code, company) + return details +def get_actual_qty(item_code, warehouse): + return frappe.db.get_value( + "Bin", + {"item_code": item_code, "warehouse": warehouse}, + "actual_qty", + ) + + def update_delivery_note_item(source, target, delivery_note): cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center") if not cost_center: diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index e7af1c6a005..a33123e3e16 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -22,6 +22,10 @@ "conversion_factor", "stock_uom", "delivered_qty", + "available_quantity_section", + "actual_qty", + "column_break_kyek", + "company_total_stock", "serial_no_and_batch_section", "pick_serial_and_batch", "serial_and_batch_bundle", @@ -124,7 +128,7 @@ "fieldname": "stock_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Stock Qty", + "label": "Qty (in Stock UOM)", "read_only": 1 }, { @@ -248,11 +252,38 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "fieldname": "available_quantity_section", + "fieldtype": "Section Break", + "label": "Available Qty" + }, + { + "allow_on_submit": 1, + "fieldname": "actual_qty", + "fieldtype": "Float", + "label": "Qty (Warehouse)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_kyek", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "company_total_stock", + "fieldtype": "Float", + "label": "Qty (Company)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2025-05-31 19:57:43.531298", + "modified": "2025-09-23 00:02:57.817040", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py index af23a424949..bdba97f4056 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.py +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.py @@ -15,7 +15,9 @@ class PickListItem(Document): if TYPE_CHECKING: from frappe.types import DF + actual_qty: DF.Float batch_no: DF.Link | None + company_total_stock: DF.Float conversion_factor: DF.Float delivered_qty: DF.Float description: DF.Text | None 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 35cdae457b8..aff309e5f80 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 @@ -2360,6 +2360,16 @@ def get_available_batches(kwargs): kwargs.posting_date, kwargs.posting_time ) + if kwargs.get("creation"): + timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( + kwargs.posting_date, kwargs.posting_time + ) + + timestamp_condition |= ( + stock_ledger_entry.posting_datetime + == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) + ) & (stock_ledger_entry.creation < kwargs.creation) + query = query.where(timestamp_condition) for field in ["warehouse", "item_code"]: @@ -2601,6 +2611,16 @@ def get_stock_ledgers_for_serial_nos(kwargs): kwargs.posting_date, kwargs.posting_time ) + if kwargs.get("creation"): + timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( + kwargs.posting_date, kwargs.posting_time + ) + + timestamp_condition |= ( + stock_ledger_entry.posting_datetime + == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) + ) & (stock_ledger_entry.creation < kwargs.creation) + query = query.where(timestamp_condition) for field in ["warehouse", "item_code", "serial_no"]: @@ -2659,6 +2679,16 @@ def get_stock_ledgers_batches(kwargs): kwargs.posting_date, kwargs.posting_time ) + if kwargs.get("creation"): + timestamp_condition = stock_ledger_entry.posting_datetime < get_combine_datetime( + kwargs.posting_date, kwargs.posting_time + ) + + timestamp_condition |= ( + stock_ledger_entry.posting_datetime + == get_combine_datetime(kwargs.posting_date, kwargs.posting_time) + ) & (stock_ledger_entry.creation < kwargs.creation) + query = query.where(timestamp_condition) if kwargs.get("ignore_voucher_nos"): diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index ec54c9dc92e..bd0a0beceef 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -5,7 +5,7 @@ import frappe from frappe import _, bold, json, msgprint from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import add_to_date, cint, cstr, flt, get_datetime +from frappe.utils import add_to_date, cint, cstr, flt, get_datetime, now import erpnext from erpnext.accounts.utils import get_company_default @@ -1034,7 +1034,7 @@ class StockReconciliation(StockController): val_rate = 0.0 current_qty = 0.0 if row.current_serial_and_batch_bundle: - current_qty = self.get_current_qty_for_serial_or_batch(row) + current_qty = self.get_current_qty_for_serial_or_batch(row, sle_creation) elif row.serial_no: item_dict = get_stock_balance_for( row.item_code, @@ -1143,17 +1143,17 @@ class StockReconciliation(StockController): return allow_negative_stock - def get_current_qty_for_serial_or_batch(self, row): + def get_current_qty_for_serial_or_batch(self, row, sle_creation): doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle) current_qty = 0.0 if doc.has_serial_no: - current_qty = self.get_current_qty_for_serial_nos(doc) + current_qty = self.get_current_qty_for_serial_nos(doc, sle_creation) elif doc.has_batch_no: - current_qty = self.get_current_qty_for_batch_nos(doc) + current_qty = self.get_current_qty_for_batch_nos(doc, sle_creation) return abs(current_qty) - def get_current_qty_for_serial_nos(self, doc): + def get_current_qty_for_serial_nos(self, doc, sle_creation): serial_nos_details = get_available_serial_nos( frappe._dict( { @@ -1161,6 +1161,7 @@ class StockReconciliation(StockController): "warehouse": doc.warehouse, "posting_date": self.posting_date, "posting_time": self.posting_time, + "creation": sle_creation, "voucher_no": self.name, "ignore_warehouse": 1, } @@ -1190,7 +1191,7 @@ class StockReconciliation(StockController): return current_qty - def get_current_qty_for_batch_nos(self, doc): + def get_current_qty_for_batch_nos(self, doc, sle_creation): current_qty = 0.0 precision = doc.entries[0].precision("qty") for d in doc.entries: @@ -1198,6 +1199,7 @@ class StockReconciliation(StockController): get_batch_qty( d.batch_no, doc.warehouse, + creation=sle_creation, posting_date=doc.posting_date, posting_time=doc.posting_time, ignore_voucher_nos=[doc.voucher_no], @@ -1494,6 +1496,7 @@ def get_stock_balance_for( "company": company, "posting_date": posting_date, "posting_time": posting_time, + "creation": row.get("creation") if row and row.get("creation") else now(), } ) ) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 22630d80f38..3ec23cf1481 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -312,7 +312,7 @@ def is_first_response(issue): def calculate_first_response_time(issue, first_responded_on): - issue_creation_date = issue.service_level_agreement_creation or issue.creation + issue_creation_date = get_datetime(issue.service_level_agreement_creation or issue.creation) issue_creation_time = get_time_in_seconds(issue_creation_date) first_responded_on_in_seconds = get_time_in_seconds(first_responded_on) support_hours = frappe.get_cached_doc( 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 26c017bddaa..6a39a434b6a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -25,7 +25,7 @@ from frappe.utils.caching import redis_cache from frappe.utils.nestedset import get_ancestors_of from frappe.utils.safe_exec import get_safe_globals -from erpnext.support.doctype.issue.issue import get_holidays +from erpnext.support.doctype.issue.issue import calculate_first_response_time, get_holidays class ServiceLevelAgreement(Document): @@ -552,6 +552,8 @@ def handle_status_change(doc, apply_sla_for_resolution): def set_first_response(): if doc.meta.has_field("first_responded_on") and not doc.get("first_responded_on"): doc.first_responded_on = now_time + if doc.meta.has_field("first_response_time"): + doc.first_response_time = calculate_first_response_time(doc, doc.first_responded_on) if get_datetime(doc.get("first_responded_on")) > get_datetime(doc.get("response_by")): record_assigned_users_on_failure(doc) diff --git a/erpnext/tests/test_activation.py b/erpnext/tests/test_activation.py index b56e2332cf4..ec210c919de 100644 --- a/erpnext/tests/test_activation.py +++ b/erpnext/tests/test_activation.py @@ -5,5 +5,6 @@ from erpnext.utilities.activation import get_level class TestActivation(FrappeTestCase): def test_activation(self): - levels = get_level() + site_info = {"activation": {"activation_level": 0, "sales_data": []}} + levels = get_level(site_info) self.assertTrue(levels) diff --git a/erpnext/utilities/__init__.py b/erpnext/utilities/__init__.py index 24bfdc63af6..f01aa1312f6 100644 --- a/erpnext/utilities/__init__.py +++ b/erpnext/utilities/__init__.py @@ -37,7 +37,7 @@ def get_site_info(site_info): if company: domain = frappe.get_cached_value("Company", cstr(company), "domain") - return {"company": company, "domain": domain, "activation": get_level()} + return {"company": company, "domain": domain, "activation": get_level(site_info)} @contextmanager diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index e09d87dd769..509c9836d3c 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -9,9 +9,9 @@ from frappe.core.doctype.installed_applications.installed_applications import ge import erpnext -def get_level(): - activation_level = 0 - sales_data = [] +def get_level(site_info): + activation_level = site_info.get("activation", {}).get("activation_level", 0) + sales_data = site_info.get("activation", {}).get("sales_data", []) min_count = 0 doctypes = { "Asset": 5,