mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 03:45:08 +00:00
Merge branch 'develop' into unit-price-contract-2
This commit is contained in:
@@ -79,9 +79,12 @@
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status"
|
||||
],
|
||||
@@ -545,6 +548,23 @@
|
||||
"fieldname": "use_sales_invoice_in_pos",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Sales Invoice"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Receivable / Payable Tuning"
|
||||
},
|
||||
{
|
||||
"fieldname": "legacy_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Legacy Fields"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -553,7 +573,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-30 20:47:17.954736",
|
||||
"modified": "2025-05-05 12:29:38.302027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -54,6 +54,7 @@ class AccountsSettings(Document):
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
|
||||
@@ -253,11 +253,20 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def validate_inter_company_accounts(self):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference)
|
||||
doc = frappe.db.get_value(
|
||||
"Journal Entry",
|
||||
self.inter_company_journal_entry_reference,
|
||||
["company", "total_debit", "total_credit"],
|
||||
as_dict=True,
|
||||
)
|
||||
account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency")
|
||||
if account_currency == previous_account_currency:
|
||||
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
|
||||
credit_precision = self.precision("total_credit")
|
||||
debit_precision = self.precision("total_debit")
|
||||
if (flt(self.total_credit, credit_precision) != flt(doc.total_debit, debit_precision)) or (
|
||||
flt(self.total_debit, debit_precision) != flt(doc.total_credit, credit_precision)
|
||||
):
|
||||
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
|
||||
|
||||
def validate_depr_entry_voucher_type(self):
|
||||
@@ -1262,9 +1271,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_bank_cash_account(
|
||||
company, account_type=None, mode_of_payment=None, account=None, ignore_permissions=False
|
||||
):
|
||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
|
||||
if mode_of_payment:
|
||||
@@ -1302,7 +1309,7 @@ def get_default_bank_cash_account(
|
||||
return frappe._dict(
|
||||
{
|
||||
"account": account,
|
||||
"balance": get_balance_on(account, ignore_account_permission=ignore_permissions),
|
||||
"balance": get_balance_on(account),
|
||||
"account_currency": account_details.account_currency,
|
||||
"account_type": account_details.account_type,
|
||||
}
|
||||
|
||||
@@ -2966,7 +2966,6 @@ def get_payment_entry(
|
||||
party_type=None,
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
ignore_permissions=False,
|
||||
created_from_payment_request=False,
|
||||
):
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
@@ -2988,14 +2987,14 @@ def get_payment_entry(
|
||||
)
|
||||
|
||||
# bank or cash
|
||||
bank = get_bank_cash_account(doc, bank_account, ignore_permissions=ignore_permissions)
|
||||
bank = get_bank_cash_account(doc, bank_account)
|
||||
|
||||
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
|
||||
if party_type in ["Customer", "Supplier"] and not bank:
|
||||
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
|
||||
if party_bank_account:
|
||||
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
|
||||
bank = get_bank_cash_account(doc, account, ignore_permissions=ignore_permissions)
|
||||
bank = get_bank_cash_account(doc, account)
|
||||
|
||||
paid_amount, received_amount = set_paid_amount_and_received_amount(
|
||||
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
||||
@@ -3306,13 +3305,12 @@ def update_accounting_dimensions(pe, doc):
|
||||
pe.set(dimension, doc.get(dimension))
|
||||
|
||||
|
||||
def get_bank_cash_account(doc, bank_account, ignore_permissions=False):
|
||||
def get_bank_cash_account(doc, bank_account):
|
||||
bank = get_default_bank_cash_account(
|
||||
doc.company,
|
||||
"Bank",
|
||||
mode_of_payment=doc.get("mode_of_payment"),
|
||||
account=bank_account,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
|
||||
if not bank:
|
||||
|
||||
@@ -672,7 +672,12 @@ def get_amount(ref_doc, payment_account=None):
|
||||
|
||||
dt = ref_doc.doctype
|
||||
if dt in ["Sales Order", "Purchase Order"]:
|
||||
grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - ref_doc.advance_paid
|
||||
advance_amount = flt(ref_doc.advance_paid)
|
||||
if ref_doc.party_account_currency != ref_doc.currency:
|
||||
advance_amount = flt(flt(ref_doc.advance_paid) / ref_doc.conversion_rate)
|
||||
|
||||
grand_total = (flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)) - advance_amount
|
||||
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if (
|
||||
dt == "Sales Invoice"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"posting_date",
|
||||
"company",
|
||||
"account",
|
||||
"group_by",
|
||||
"categorize_by",
|
||||
"cost_center",
|
||||
"territory",
|
||||
"ignore_exchange_rate_revaluation_journals",
|
||||
@@ -174,14 +174,6 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Start Date"
|
||||
},
|
||||
{
|
||||
"default": "Group by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "group_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Group By",
|
||||
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'General Ledger');",
|
||||
"fieldname": "currency",
|
||||
@@ -397,10 +389,18 @@
|
||||
"fieldname": "show_remarks",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Remarks"
|
||||
},
|
||||
{
|
||||
"default": "Categorize by Voucher (Consolidated)",
|
||||
"depends_on": "eval:(doc.report == 'General Ledger');",
|
||||
"fieldname": "categorize_by",
|
||||
"fieldtype": "Select",
|
||||
"label": "Categorize By",
|
||||
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-12-11 12:11:13.543134",
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
@@ -432,8 +432,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
ageing_based_on: DF.Literal["Due Date", "Posting Date"]
|
||||
based_on_payment_terms: DF.Check
|
||||
body: DF.TextEditor | None
|
||||
categorize_by: DF.Literal["", "Categorize by Voucher", "Categorize by Voucher (Consolidated)"]
|
||||
cc_to: DF.TableMultiSelect[ProcessStatementOfAccountsCC]
|
||||
collection_name: DF.DynamicLink | None
|
||||
company: DF.Link
|
||||
@@ -56,7 +57,6 @@ class ProcessStatementOfAccounts(Document):
|
||||
finance_book: DF.Link | None
|
||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||
from_date: DF.Date | None
|
||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||
ignore_cr_dr_notes: DF.Check
|
||||
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||
include_ageing: DF.Check
|
||||
@@ -204,7 +204,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
||||
"party": [entry.customer],
|
||||
"party_name": [entry.customer_name] if entry.customer_name else None,
|
||||
"presentation_currency": presentation_currency,
|
||||
"group_by": doc.group_by,
|
||||
"categorize_by": doc.categorize_by,
|
||||
"currency": doc.currency,
|
||||
"project": [p.project_name for p in doc.project],
|
||||
"show_opening_entries": 0,
|
||||
|
||||
@@ -1421,7 +1421,7 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
|
||||
@@ -54,6 +54,10 @@ class ReceivablePayableReport:
|
||||
self.filters.range = "30, 60, 90, 120"
|
||||
self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
|
||||
self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
|
||||
self.ple_fetch_method = (
|
||||
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||
or "Buffered Cursor"
|
||||
) # Fail Safe
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
@@ -90,10 +94,7 @@ class ReceivablePayableReport:
|
||||
self.skip_total_row = 1
|
||||
|
||||
def get_data(self):
|
||||
self.get_ple_entries()
|
||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||
self.voucher_balance = OrderedDict()
|
||||
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
@@ -110,12 +111,40 @@ class ReceivablePayableReport:
|
||||
# Get Exchange Rate Revaluations
|
||||
self.get_exchange_rate_revaluations()
|
||||
|
||||
self.prepare_ple_query()
|
||||
self.data = []
|
||||
self.voucher_balance = OrderedDict()
|
||||
|
||||
if self.ple_fetch_method == "Buffered Cursor":
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
|
||||
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
|
||||
self.build_data()
|
||||
delattr(self, "ple_entries")
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
# This is unavoidable. Initialization and allocation cannot happen in same loop
|
||||
for ple in self.ple_entries:
|
||||
self.update_voucher_balance(ple)
|
||||
delattr(self, "ple_entries")
|
||||
|
||||
def build_voucher_dict(self, ple):
|
||||
return frappe._dict(
|
||||
@@ -136,26 +165,22 @@ class ReceivablePayableReport:
|
||||
outstanding_in_account_currency=0.0,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
# get the balance object for voucher_type
|
||||
def init_voucher_balance(self, ple):
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
else:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
self.get_invoices(ple)
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
||||
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
|
||||
self.init_subtotal_row("Total")
|
||||
@@ -778,7 +803,7 @@ class ReceivablePayableReport:
|
||||
)
|
||||
row["range" + str(index + 1)] = row.outstanding
|
||||
|
||||
def get_ple_entries(self):
|
||||
def prepare_ple_query(self):
|
||||
# get all the GL entries filtered by the given filters
|
||||
|
||||
self.prepare_conditions()
|
||||
@@ -831,7 +856,7 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||
|
||||
self.ple_entries = query.run(as_dict=True)
|
||||
self.ple_query = query
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
|
||||
@@ -144,10 +144,10 @@ class PartyLedgerSummaryReport:
|
||||
if self.party_naming_by == "Naming Series":
|
||||
columns.append(
|
||||
{
|
||||
"label": _(self.filters.party_type + "Name"),
|
||||
"label": _(self.filters.party_type + " Name"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "party_name",
|
||||
"width": 110,
|
||||
"width": 150,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -252,12 +252,13 @@ class PartyLedgerSummaryReport:
|
||||
self.party_data = frappe._dict({})
|
||||
for gle in self.gl_entries:
|
||||
party_details = self.party_details.get(gle.party)
|
||||
party_name = party_details.get(f"{scrub(self.filters.party_type)}_name", "")
|
||||
self.party_data.setdefault(
|
||||
gle.party,
|
||||
frappe._dict(
|
||||
{
|
||||
**party_details,
|
||||
"party_name": gle.party,
|
||||
"party_name": party_name,
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 0,
|
||||
"paid_amount": 0,
|
||||
|
||||
@@ -49,7 +49,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Voucher No"),
|
||||
fieldtype: "Data",
|
||||
on_change: function () {
|
||||
frappe.query_report.set_filter_value("group_by", "Group by Voucher (Consolidated)");
|
||||
frappe.query_report.set_filter_value("categorize_by", "Categorize by Voucher (Consolidated)");
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -112,29 +112,29 @@ frappe.query_reports["General Ledger"] = {
|
||||
hidden: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group by"),
|
||||
fieldname: "categorize_by",
|
||||
label: __("Categorize by"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
"",
|
||||
{
|
||||
label: __("Group by Voucher"),
|
||||
value: "Group by Voucher",
|
||||
label: __("Categorize by Voucher"),
|
||||
value: "Categorize by Voucher",
|
||||
},
|
||||
{
|
||||
label: __("Group by Voucher (Consolidated)"),
|
||||
value: "Group by Voucher (Consolidated)",
|
||||
label: __("Categorize by Voucher (Consolidated)"),
|
||||
value: "Categorize by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
label: __("Group by Account"),
|
||||
value: "Group by Account",
|
||||
label: __("Categorize by Account"),
|
||||
value: "Categorize by Account",
|
||||
},
|
||||
{
|
||||
label: __("Group by Party"),
|
||||
value: "Group by Party",
|
||||
label: __("Categorize by Party"),
|
||||
value: "Categorize by Party",
|
||||
},
|
||||
],
|
||||
default: "Group by Voucher (Consolidated)",
|
||||
default: "Categorize by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
fieldname: "tax_id",
|
||||
|
||||
@@ -63,13 +63,13 @@ def validate_filters(filters, account_details):
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if filters.get("account") and filters.get("group_by") == "Group by Account":
|
||||
if filters.get("account") and filters.get("categorize_by") == "Categorize by Account":
|
||||
filters.account = frappe.parse_json(filters.get("account"))
|
||||
for account in filters.account:
|
||||
if account_details[account].is_group == 0:
|
||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||
|
||||
if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]:
|
||||
if filters.get("voucher_no") and filters.get("categorize_by") in ["Categorize by Voucher"]:
|
||||
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
@@ -163,9 +163,9 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
if filters.get("include_dimensions"):
|
||||
order_by_statement = "order by posting_date, creation"
|
||||
|
||||
if filters.get("group_by") == "Group by Voucher":
|
||||
if filters.get("categorize_by") == "Categorize by Voucher":
|
||||
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
||||
if filters.get("group_by") == "Group by Account":
|
||||
if filters.get("categorize_by") == "Categorize by Account":
|
||||
order_by_statement = "order by account, posting_date, creation"
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
@@ -260,7 +260,7 @@ def get_conditions(filters):
|
||||
if filters.get("voucher_no_not_in"):
|
||||
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||
|
||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
||||
if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"):
|
||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||
|
||||
if filters.get("party_type"):
|
||||
@@ -272,7 +272,7 @@ def get_conditions(filters):
|
||||
if not (
|
||||
filters.get("account")
|
||||
or filters.get("party")
|
||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
||||
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
|
||||
):
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
@@ -374,26 +374,26 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# Opening for filtered account
|
||||
data.append(totals.opening)
|
||||
|
||||
if filters.get("group_by") != "Group by Voucher (Consolidated)":
|
||||
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
|
||||
for _acc, acc_dict in gle_map.items():
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None})
|
||||
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"
|
||||
if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
|
||||
):
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
data += acc_dict.entries
|
||||
|
||||
# totals
|
||||
if filters.get("group_by") or not filters.voucher_no:
|
||||
if filters.get("categorize_by") or not filters.voucher_no:
|
||||
data.append(acc_dict.totals.total)
|
||||
|
||||
# closing
|
||||
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"
|
||||
if (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
|
||||
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
|
||||
):
|
||||
data.append(acc_dict.totals.closing)
|
||||
|
||||
@@ -430,9 +430,9 @@ def get_totals_dict():
|
||||
|
||||
|
||||
def group_by_field(group_by):
|
||||
if group_by == "Group by Party":
|
||||
if group_by == "Categorize by Party":
|
||||
return "party"
|
||||
elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]:
|
||||
elif group_by in ["Categorize by Voucher (Consolidated)", "Categorize by Account"]:
|
||||
return "account"
|
||||
else:
|
||||
return "voucher_no"
|
||||
@@ -440,7 +440,7 @@ def group_by_field(group_by):
|
||||
|
||||
def initialize_gle_map(gl_entries, filters, totals_dict):
|
||||
gle_map = OrderedDict()
|
||||
group_by = group_by_field(filters.get("group_by"))
|
||||
group_by = group_by_field(filters.get("categorize_by"))
|
||||
|
||||
for gle in gl_entries:
|
||||
gle_map.setdefault(gle.get(group_by), _dict(totals=copy.deepcopy(totals_dict), entries=[]))
|
||||
@@ -450,8 +450,8 @@ def initialize_gle_map(gl_entries, filters, totals_dict):
|
||||
def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals):
|
||||
entries = []
|
||||
consolidated_gle = OrderedDict()
|
||||
group_by = group_by_field(filters.get("group_by"))
|
||||
group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)"
|
||||
group_by = group_by_field(filters.get("categorize_by"))
|
||||
group_by_voucher_consolidated = filters.get("categorize_by") == "Categorize by Voucher (Consolidated)"
|
||||
|
||||
if filters.get("show_net_values_in_party_account"):
|
||||
account_type_map = get_account_type_map(filters.get("company"))
|
||||
|
||||
@@ -155,7 +155,7 @@ class TestGeneralLedger(IntegrationTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -246,7 +246,7 @@ class TestGeneralLedger(IntegrationTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_err": True,
|
||||
}
|
||||
)
|
||||
@@ -261,7 +261,7 @@ class TestGeneralLedger(IntegrationTestCase):
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"account": [account.name],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_err": False,
|
||||
}
|
||||
)
|
||||
@@ -308,7 +308,7 @@ class TestGeneralLedger(IntegrationTestCase):
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": False,
|
||||
}
|
||||
)
|
||||
@@ -325,7 +325,7 @@ class TestGeneralLedger(IntegrationTestCase):
|
||||
"from_date": si.posting_date,
|
||||
"to_date": si.posting_date,
|
||||
"account": [si.debit_to],
|
||||
"group_by": "Group by Voucher (Consolidated)",
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
"ignore_cr_dr_notes": True,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -152,6 +152,9 @@ class AssetMovement(Document):
|
||||
""",
|
||||
args,
|
||||
)
|
||||
|
||||
self.validate_movement_cancellation(d, latest_movement_entry)
|
||||
|
||||
if latest_movement_entry:
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
@@ -179,3 +182,12 @@ class AssetMovement(Document):
|
||||
d.asset,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
|
||||
)
|
||||
|
||||
def validate_movement_cancellation(self, row, latest_movement_entry):
|
||||
asset_doc = frappe.get_doc("Asset", row.asset)
|
||||
if not latest_movement_entry and asset_doc.docstatus == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset {0} has only one movement record. Please create another movement before deleting this one to maintain asset tracking."
|
||||
).format(row.asset)
|
||||
)
|
||||
|
||||
@@ -147,6 +147,45 @@ class TestAssetMovement(IntegrationTestCase):
|
||||
movement1.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||
|
||||
def test_last_movement_cancellation_validation(self):
|
||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
asset.calculate_depreciation = 1
|
||||
asset.available_for_use_date = "2020-06-06"
|
||||
asset.purchase_date = "2020-06-06"
|
||||
asset.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"next_depreciation_date": "2020-12-31",
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 3,
|
||||
"frequency_of_depreciation": 10,
|
||||
},
|
||||
)
|
||||
if asset.docstatus == 0:
|
||||
asset.submit()
|
||||
|
||||
AssetMovement = frappe.qb.DocType("Asset Movement")
|
||||
AssetMovementItem = frappe.qb.DocType("Asset Movement Item")
|
||||
|
||||
asset_movement = (
|
||||
frappe.qb.from_(AssetMovement)
|
||||
.join(AssetMovementItem)
|
||||
.on(AssetMovementItem.parent == AssetMovement.name)
|
||||
.select(AssetMovement.name)
|
||||
.where(
|
||||
(AssetMovementItem.asset == asset.name)
|
||||
& (AssetMovement.company == asset.company)
|
||||
& (AssetMovement.docstatus == 1)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
asset_movement_doc = frappe.get_doc("Asset Movement", asset_movement[0].name)
|
||||
self.assertRaises(frappe.ValidationError, asset_movement_doc.cancel)
|
||||
|
||||
|
||||
def create_asset_movement(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@@ -261,15 +262,16 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:33.272106",
|
||||
"modified": "2025-04-28 23:30:22.927989",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ frappe.ui.form.on("Supplier", {
|
||||
address_dict: frm.doc.supplier_primary_address,
|
||||
},
|
||||
callback: function (r) {
|
||||
frm.set_value("primary_address", r.message);
|
||||
frm.set_value("primary_address", frappe.utils.html2text(r.message));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,6 +250,7 @@ def make_purchase_order(source_name, target_doc=None):
|
||||
{
|
||||
"Supplier Quotation": {
|
||||
"doctype": "Purchase Order",
|
||||
"field_no_map": ["transaction_date"],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
@@ -57,7 +58,7 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
|
||||
for doc in po.get("items"):
|
||||
if doc.get("item_code"):
|
||||
doc.set("schedule_date", "2013-04-12")
|
||||
doc.set("schedule_date", add_days(today(), 1))
|
||||
|
||||
po.insert()
|
||||
|
||||
|
||||
@@ -76,14 +76,14 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group by"),
|
||||
fieldname: "categorize_by",
|
||||
label: __("Categorize by"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ label: __("Group by Supplier"), value: "Group by Supplier" },
|
||||
{ label: __("Group by Item"), value: "Group by Item" },
|
||||
{ label: __("Categorize by Supplier"), value: "Categorize by Supplier" },
|
||||
{ label: __("Categorize by Item"), value: "Categorize by Item" },
|
||||
],
|
||||
default: __("Group by Supplier"),
|
||||
default: __("Categorize by Supplier"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
|
||||
@@ -82,20 +82,14 @@ def prepare_data(supplier_quotation_data, filters):
|
||||
group_wise_map = defaultdict(list)
|
||||
supplier_qty_price_map = {}
|
||||
|
||||
group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code"
|
||||
company_currency = frappe.db.get_default("currency")
|
||||
group_by_field = (
|
||||
"supplier_name" if filters.get("categorize_by") == "Categorize by Supplier" else "item_code"
|
||||
)
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
|
||||
for data in supplier_quotation_data:
|
||||
group = data.get(group_by_field) # get item or supplier value for this row
|
||||
|
||||
supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency")
|
||||
|
||||
if supplier_currency:
|
||||
exchange_rate = get_exchange_rate(supplier_currency, company_currency)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
|
||||
row = {
|
||||
"item_code": ""
|
||||
if group_by_field == "item_code"
|
||||
@@ -103,7 +97,7 @@ def prepare_data(supplier_quotation_data, filters):
|
||||
"supplier_name": "" if group_by_field == "supplier_name" else data.get("supplier_name"),
|
||||
"quotation": data.get("parent"),
|
||||
"qty": data.get("qty"),
|
||||
"price": flt(data.get("amount") * exchange_rate, float_precision),
|
||||
"price": flt(data.get("amount"), float_precision),
|
||||
"uom": data.get("uom"),
|
||||
"price_list_currency": data.get("price_list_currency"),
|
||||
"currency": data.get("currency"),
|
||||
@@ -209,6 +203,13 @@ def get_columns(filters):
|
||||
columns = [
|
||||
{"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90},
|
||||
{"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"label": _("Stock UOM"),
|
||||
"fieldtype": "Link",
|
||||
"options": "UOM",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
@@ -223,13 +224,6 @@ def get_columns(filters):
|
||||
"options": "currency",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"label": _("Stock UOM"),
|
||||
"fieldtype": "Link",
|
||||
"options": "UOM",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"fieldname": "price_per_unit",
|
||||
"label": _("Price per Unit (Stock UOM)"),
|
||||
@@ -274,7 +268,7 @@ def get_columns(filters):
|
||||
},
|
||||
]
|
||||
|
||||
if filters.get("group_by") == "Group by Item":
|
||||
if filters.get("categorize_by") == "Categorize by Item":
|
||||
group_by_columns.reverse()
|
||||
|
||||
columns[0:0] = group_by_columns # add positioned group by columns to the report
|
||||
|
||||
@@ -98,7 +98,29 @@ class BuyingController(SubcontractingController):
|
||||
item.from_warehouse,
|
||||
type_of_transaction="Outward",
|
||||
do_not_submit=True,
|
||||
qty=item.qty,
|
||||
)
|
||||
elif (
|
||||
not self.is_new()
|
||||
and item.serial_and_batch_bundle
|
||||
and next(
|
||||
(
|
||||
old_item
|
||||
for old_item in self.get_doc_before_save().items
|
||||
if old_item.name == item.name and old_item.qty != item.qty
|
||||
),
|
||||
None,
|
||||
)
|
||||
and len(
|
||||
sabe := frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": item.serial_and_batch_bundle, "serial_no": ["is", "not set"]},
|
||||
pluck="name",
|
||||
)
|
||||
)
|
||||
== 1
|
||||
):
|
||||
frappe.set_value("Serial and Batch Entry", sabe[0], "qty", item.qty)
|
||||
|
||||
def set_rate_for_standalone_debit_note(self):
|
||||
if self.get("is_return") and self.get("update_stock") and not self.return_against:
|
||||
|
||||
@@ -811,7 +811,7 @@ class StockController(AccountsController):
|
||||
)
|
||||
|
||||
def make_package_for_transfer(
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None, qty=0
|
||||
):
|
||||
return make_bundle_for_material_transfer(
|
||||
is_new=self.is_new(),
|
||||
@@ -822,6 +822,7 @@ class StockController(AccountsController):
|
||||
warehouse=warehouse,
|
||||
type_of_transaction=type_of_transaction,
|
||||
do_not_submit=do_not_submit,
|
||||
qty=qty,
|
||||
)
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
@@ -1818,15 +1819,24 @@ def make_bundle_for_material_transfer(**kwargs):
|
||||
kwargs.type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.docstatus = 0
|
||||
bundle_doc.warehouse = kwargs.warehouse
|
||||
bundle_doc.type_of_transaction = kwargs.type_of_transaction
|
||||
bundle_doc.voucher_type = kwargs.voucher_type
|
||||
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
qty = 0
|
||||
if (
|
||||
len(bundle_doc.entries) == 1
|
||||
and flt(kwargs.qty) < flt(bundle_doc.total_qty)
|
||||
and not bundle_doc.has_serial_no
|
||||
):
|
||||
qty = kwargs.qty
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.qty = abs(qty or row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if kwargs.type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-04-27 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-04-28 06:21\n"
|
||||
"PO-Revision-Date: 2025-04-30 07:12\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Persian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -6585,7 +6585,7 @@ msgstr "مقدار موجود"
|
||||
#. Name of a report
|
||||
#: erpnext/stock/report/available_serial_no/available_serial_no.json
|
||||
msgid "Available Serial No"
|
||||
msgstr ""
|
||||
msgstr "شماره سریال موجود"
|
||||
|
||||
#: erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py:38
|
||||
msgid "Available Stock"
|
||||
@@ -9105,7 +9105,7 @@ msgstr "فقط در صورتی میتوان ردیف را ارجاع داد
|
||||
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.py:137
|
||||
msgid "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method"
|
||||
msgstr "نمیتوان روش ارزش گذاری را تغییر داد، زیرا معاملاتی در برابر برخی اقلام وجود دارد که روش ارزش گذاری خاص خود را ندارند."
|
||||
msgstr "نمیتوان روش ارزش گذاری را تغییر داد، زیرا تراکنشهایی در برابر برخی آیتمها وجود دارد که روش ارزش گذاری خاص خود را ندارند"
|
||||
|
||||
#: erpnext/templates/pages/task_info.html:24
|
||||
msgid "Cancel"
|
||||
@@ -11501,7 +11501,7 @@ msgstr "کنش را طوری پیکربندی کنید که تراکنش را م
|
||||
|
||||
#: erpnext/buying/doctype/buying_settings/buying_settings.js:20
|
||||
msgid "Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List."
|
||||
msgstr "هنگام ایجاد تراکنش خرید جدید، فهرست قیمت پیشفرض را پیکربندی کنید. قیمت اقلام از این لیست قیمت دریافت میشود."
|
||||
msgstr "هنگام ایجاد تراکنش خرید جدید، فهرست قیمت پیشفرض را پیکربندی کنید. قیمت آیتم از این لیست قیمت دریافت میشود."
|
||||
|
||||
#. Label of the final_confirmation_date (Date) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
@@ -12183,7 +12183,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:2767
|
||||
msgid "Conversion rate must be 1.00 if document currency is same as company currency"
|
||||
msgstr ""
|
||||
msgstr "اگر واحد پول سند با واحد پول شرکت یکسان باشد، نرخ تبدیل باید 1.00 باشد"
|
||||
|
||||
#. Label of the clean_description_html (Check) field in DocType 'Stock
|
||||
#. Settings'
|
||||
@@ -15570,7 +15570,7 @@ msgstr "تنظیمات پیشفرض برای تراکنشهای مربوط
|
||||
|
||||
#: erpnext/setup/doctype/company/company.js:168
|
||||
msgid "Default tax templates for sales, purchase and items are created."
|
||||
msgstr "الگوهای مالیاتی پیشفرض برای فروش، خرید و اقلام ایجاد میشود."
|
||||
msgstr "الگوهای مالیاتی پیشفرض برای فروش، خرید و آیتمها ایجاد میشود."
|
||||
|
||||
#. Description of the 'Time Between Operations (Mins)' (Int) field in DocType
|
||||
#. 'Manufacturing Settings'
|
||||
@@ -17989,7 +17989,7 @@ msgstr "تکرار پروژه با تسکها"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:144
|
||||
msgid "Duplicate Sales Invoices found"
|
||||
msgstr ""
|
||||
msgstr "فاکتورهای فروش تکراری پیدا شد"
|
||||
|
||||
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:78
|
||||
msgid "Duplicate Stock Closing Entry"
|
||||
@@ -21863,12 +21863,12 @@ msgstr "دریافت آیتمها از رسید خرید"
|
||||
#. Label of the transfer_materials (Button) field in DocType 'Production Plan'
|
||||
#: erpnext/manufacturing/doctype/production_plan/production_plan.json
|
||||
msgid "Get Items for Purchase / Transfer"
|
||||
msgstr ""
|
||||
msgstr "دریافت آیتمها برای خرید / انتقال"
|
||||
|
||||
#. Label of the get_items_for_mr (Button) field in DocType 'Production Plan'
|
||||
#: erpnext/manufacturing/doctype/production_plan/production_plan.json
|
||||
msgid "Get Items for Purchase Only"
|
||||
msgstr ""
|
||||
msgstr "دریافت آیتمها فقط برای خرید"
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.js:310
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.js:656
|
||||
@@ -23218,7 +23218,7 @@ msgstr "اگر آیتم گونهای از یک آیتم دیگر باشد،
|
||||
#. DocType 'Production Plan'
|
||||
#: erpnext/manufacturing/doctype/production_plan/production_plan.json
|
||||
msgid "If items in stock, proceed with Material Transfer or Purchase."
|
||||
msgstr ""
|
||||
msgstr "اگر آیتمها موجود هستند، مراحل انتقال مواد یا خرید را ادامه دهید."
|
||||
|
||||
#. Description of the 'Role Allowed to Create/Edit Back-dated Transactions'
|
||||
#. (Link) field in DocType 'Stock Settings'
|
||||
@@ -24931,7 +24931,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:194
|
||||
msgid "Invalid Sales Invoices"
|
||||
msgstr ""
|
||||
msgstr "فاکتورهای فروش نامعتبر"
|
||||
|
||||
#: erpnext/assets/doctype/asset/asset.py:460
|
||||
#: erpnext/assets/doctype/asset/asset.py:467
|
||||
@@ -24962,7 +24962,7 @@ msgstr "انبار نامعتبر"
|
||||
|
||||
#: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:355
|
||||
msgid "Invalid amount in accounting entries of {} {} for Account {}: {}"
|
||||
msgstr ""
|
||||
msgstr "مبلغ نامعتبر در ثبتهای حسابداری {} {} برای حساب {}: {}"
|
||||
|
||||
#: erpnext/accounts/doctype/pricing_rule/pricing_rule.py:312
|
||||
msgid "Invalid condition expression"
|
||||
@@ -26990,7 +26990,7 @@ msgstr "نرخ آیتم به صفر بهروزرسانی شده است زیر
|
||||
#. Label of the finished_good (Link) field in DocType 'Job Card'
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.json
|
||||
msgid "Item to Manufacture"
|
||||
msgstr ""
|
||||
msgstr "آیتم برای تولید"
|
||||
|
||||
#. Description of the 'Item' (Link) field in DocType 'BOM'
|
||||
#: erpnext/manufacturing/doctype/bom/bom.json
|
||||
@@ -27265,7 +27265,7 @@ msgstr "آیتمها برای درخواست مواد خام"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:849
|
||||
msgid "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}"
|
||||
msgstr "نرخ آیتمها به صفر بهروزرسانی شده است زیرا نرخ ارزش گذاری مجاز صفر برای موارد زیر بررسی میشود: {0}"
|
||||
msgstr "نرخ آیتمها به صفر بهروزرسانی شده است زیرا نرخ ارزش گذاری مجاز صفر برای آیتمهای زیر بررسی میشود: {0}"
|
||||
|
||||
#. Label of the items_to_be_repost (Code) field in DocType 'Repost Item
|
||||
#. Valuation'
|
||||
@@ -27275,7 +27275,7 @@ msgstr "مواردی که باید بازنشر شوند"
|
||||
|
||||
#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1560
|
||||
msgid "Items to Manufacture are required to pull the Raw Materials associated with it."
|
||||
msgstr "اقلام برای تولید برای کشیدن مواد خام مرتبط با آن مورد نیاز است."
|
||||
msgstr "آیتم برای تولید برای دریافت مواد خام مرتبط با آن مورد نیاز است."
|
||||
|
||||
#. Label of a Link in the Buying Workspace
|
||||
#: erpnext/buying/workspace/buying/buying.json
|
||||
@@ -31799,7 +31799,7 @@ msgstr "بدون ملاحظات"
|
||||
|
||||
#: erpnext/public/js/utils/unreconcile.js:147
|
||||
msgid "No Selection"
|
||||
msgstr ""
|
||||
msgstr "بدون انتخاب"
|
||||
|
||||
#: erpnext/controllers/sales_and_purchase_return.py:919
|
||||
msgid "No Serial / Batches are available for return"
|
||||
@@ -37402,7 +37402,7 @@ msgstr "لطفاً انبار هدف را در کارت کار تنظیم کنی
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:1447
|
||||
msgid "Please set the WIP Warehouse in the Job Card"
|
||||
msgstr ""
|
||||
msgstr "لطفاً انبار کار در حال انجام را در کارت کار تنظیم کنید"
|
||||
|
||||
#: erpnext/accounts/doctype/gl_entry/gl_entry.py:174
|
||||
msgid "Please set the cost center field in {0} or setup a default Cost Center for the Company."
|
||||
@@ -40049,7 +40049,7 @@ msgstr "کالای رسید خرید"
|
||||
#. Name of a DocType
|
||||
#: erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
|
||||
msgid "Purchase Receipt Item Supplied"
|
||||
msgstr "اقلام رسید خرید تامین شد"
|
||||
msgstr "آیتم رسید خرید تامین شد"
|
||||
|
||||
#. Label of the purchase_receipt_items (Section Break) field in DocType 'Landed
|
||||
#. Cost Voucher'
|
||||
@@ -43189,7 +43189,7 @@ msgstr "ارسال مجدد دفتر پرداخت"
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/repost_payment_ledger_items/repost_payment_ledger_items.json
|
||||
msgid "Repost Payment Ledger Items"
|
||||
msgstr "ارسال مجدد اقلام دفتر پرداخت"
|
||||
msgstr "ارسال مجدد آیتمهای دفتر پرداخت"
|
||||
|
||||
#. Label of the repost_status (Select) field in DocType 'Repost Payment Ledger'
|
||||
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
|
||||
@@ -43848,7 +43848,7 @@ msgstr "محدود کردن"
|
||||
#. Item'
|
||||
#: erpnext/selling/doctype/party_specific_item/party_specific_item.json
|
||||
msgid "Restrict Items Based On"
|
||||
msgstr "موارد را بر اساس محدود کنید"
|
||||
msgstr "محدود کردن آیتمها بر اساس"
|
||||
|
||||
#. Label of the section_break_6 (Section Break) field in DocType 'Shipping
|
||||
#. Rule'
|
||||
@@ -44146,7 +44146,7 @@ msgstr ""
|
||||
#: erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
|
||||
#: erpnext/quality_management/doctype/quality_review/quality_review.json
|
||||
msgid "Reviews"
|
||||
msgstr "بررسی ها"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the rgt (Int) field in DocType 'Account'
|
||||
#. Label of the rgt (Int) field in DocType 'Company'
|
||||
@@ -44900,7 +44900,7 @@ msgstr "ردیف #{idx}: هنگام تامین مواد خام به پیمانک
|
||||
|
||||
#: erpnext/controllers/buying_controller.py:382
|
||||
msgid "Row #{idx}: Item rate has been updated as per valuation rate since its an internal stock transfer."
|
||||
msgstr "ردیف #{idx}: نرخ اقلام براساس نرخ ارزیابی بهروزرسانی شده است، زیرا یک انتقال داخلی موجودی است."
|
||||
msgstr "ردیف #{idx}: نرخ آیتم براساس نرخ ارزیابی بهروزرسانی شده است، زیرا یک انتقال داخلی موجودی است."
|
||||
|
||||
#: erpnext/controllers/buying_controller.py:846
|
||||
msgid "Row #{idx}: Please enter a location for the asset item {item_code}."
|
||||
@@ -44916,11 +44916,11 @@ msgstr "ردیف #{idx}: {field_label} نمیتواند برای مورد {it
|
||||
|
||||
#: erpnext/controllers/buying_controller.py:472
|
||||
msgid "Row #{idx}: {field_label} is mandatory."
|
||||
msgstr ""
|
||||
msgstr "ردیف #{idx}: {field_label} اجباری است."
|
||||
|
||||
#: erpnext/controllers/buying_controller.py:494
|
||||
msgid "Row #{idx}: {field_label} is not allowed in Purchase Return."
|
||||
msgstr ""
|
||||
msgstr "ردیف #{idx}: {field_label} در مرجوعی خرید مجاز نیست."
|
||||
|
||||
#: erpnext/controllers/buying_controller.py:227
|
||||
msgid "Row #{idx}: {from_warehouse_field} and {to_warehouse_field} cannot be same."
|
||||
@@ -45659,7 +45659,7 @@ msgstr "پرداخت فاکتور فروش"
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/sales_invoice_reference/sales_invoice_reference.json
|
||||
msgid "Sales Invoice Reference"
|
||||
msgstr ""
|
||||
msgstr "مرجع فاکتور فروش"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json
|
||||
@@ -45670,7 +45670,7 @@ msgstr "جدول زمانی فاکتور فروش"
|
||||
#. Closing Entry'
|
||||
#: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
|
||||
msgid "Sales Invoice Transactions"
|
||||
msgstr ""
|
||||
msgstr "تراکنشهای فاکتور فروش"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Link in the Financial Reports Workspace
|
||||
@@ -45683,7 +45683,7 @@ msgstr "روند فاکتور فروش"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:169
|
||||
msgid "Sales Invoice does not have Payments"
|
||||
msgstr ""
|
||||
msgstr "فاکتور فروش، پرداخت ندارد"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:165
|
||||
msgid "Sales Invoice is already consolidated"
|
||||
@@ -45695,11 +45695,11 @@ msgstr ""
|
||||
|
||||
#: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:177
|
||||
msgid "Sales Invoice is not submitted"
|
||||
msgstr ""
|
||||
msgstr "فاکتور فروش ارسال نشده است"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:180
|
||||
msgid "Sales Invoice isn't created by user {}"
|
||||
msgstr ""
|
||||
msgstr "فاکتور فروش توسط کاربر {} ایجاد نشده است"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:429
|
||||
msgid "Sales Invoice mode is activated in POS. Please create Sales Invoice instead."
|
||||
@@ -46616,7 +46616,7 @@ msgstr "برنامه ریزی"
|
||||
|
||||
#: erpnext/utilities/doctype/rename_tool/rename_tool.js:31
|
||||
msgid "Scheduling..."
|
||||
msgstr "برنامه ریزی..."
|
||||
msgstr "در حال زمانبندی..."
|
||||
|
||||
#. Label of the school_univ (Small Text) field in DocType 'Employee Education'
|
||||
#: erpnext/setup/doctype/employee_education/employee_education.json
|
||||
@@ -46923,7 +46923,7 @@ msgstr "کالای تمام شده را انتخاب کنید"
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.js:1161
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.js:1173
|
||||
msgid "Select Items"
|
||||
msgstr "موارد را انتخاب کنید"
|
||||
msgstr "انتخاب آیتمها"
|
||||
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.js:1047
|
||||
msgid "Select Items based on Delivery Date"
|
||||
@@ -46931,7 +46931,7 @@ msgstr "آیتمها را بر اساس تاریخ تحویل انتخاب ک
|
||||
|
||||
#: erpnext/public/js/controllers/transaction.js:2403
|
||||
msgid "Select Items for Quality Inspection"
|
||||
msgstr "موارد را برای بازرسی کیفیت انتخاب کنید"
|
||||
msgstr "انتخاب آیتمها برای بازرسی کیفیت"
|
||||
|
||||
#. Label of the select_items_to_manufacture_section (Section Break) field in
|
||||
#. DocType 'Production Plan'
|
||||
@@ -47034,7 +47034,7 @@ msgstr "یک تامین کننده انتخاب کنید"
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.js:380
|
||||
msgid "Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only."
|
||||
msgstr "یک تامین کننده از تامین کنندگان پیشفرض موارد زیر انتخاب کنید. در صورت انتخاب، یک سفارش خرید فقط در برابر اقلام متعلق به تامین کننده منتخب انجام میشود."
|
||||
msgstr "یک تامین کننده از تامین کنندگان پیشفرض آیتمهای زیر انتخاب کنید. در صورت انتخاب، یک سفارش خرید فقط در برابر آیتمهای متعلق به تامین کننده منتخب انجام میشود."
|
||||
|
||||
#: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:161
|
||||
msgid "Select a company"
|
||||
@@ -53544,7 +53544,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:612
|
||||
msgid "The items {0} and {1} are present in the following {2} :"
|
||||
msgstr "موارد {0} و {1} در {2} زیر موجود هستند:"
|
||||
msgstr "آیتمهای {0} و {1} در {2} زیر موجود هستند:"
|
||||
|
||||
#: erpnext/controllers/buying_controller.py:1024
|
||||
msgid "The items {items} are not marked as {type_of} item. You can enable them as {type_of} item from their Item masters."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-04-27 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-04-28 06:21\n"
|
||||
"PO-Revision-Date: 2025-04-29 06:31\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Portuguese, Brazilian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -44133,7 +44133,7 @@ msgstr "Revisão e Ação"
|
||||
#: erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
|
||||
#: erpnext/quality_management/doctype/quality_review/quality_review.json
|
||||
msgid "Reviews"
|
||||
msgstr ""
|
||||
msgstr "Rever"
|
||||
|
||||
#. Label of the rgt (Int) field in DocType 'Account'
|
||||
#. Label of the rgt (Int) field in DocType 'Company'
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-04-27 09:35+0000\n"
|
||||
"PO-Revision-Date: 2025-04-28 06:21\n"
|
||||
"PO-Revision-Date: 2025-04-30 07:12\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Swedish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12572,7 +12572,7 @@ msgstr "Resultat Enhet & Budget"
|
||||
|
||||
#: erpnext/public/js/utils/sales_common.js:491
|
||||
msgid "Cost Center for Item rows has been updated to {0}"
|
||||
msgstr "Resultat Enhet för artikel rader har uppdaterats till {0}"
|
||||
msgstr "Resultat Enhet för artikel rader är uppdaterad till {0}"
|
||||
|
||||
#: erpnext/accounts/doctype/cost_center/cost_center.py:75
|
||||
msgid "Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group"
|
||||
@@ -14976,7 +14976,7 @@ msgstr "Dagar Sedan Senaste Order"
|
||||
#. Label of the days_until_due (Int) field in DocType 'Subscription'
|
||||
#: erpnext/accounts/doctype/subscription/subscription.json
|
||||
msgid "Days Until Due"
|
||||
msgstr "Dagar till Förfallo datum"
|
||||
msgstr "Dagar till Förfallodatum"
|
||||
|
||||
#. Option for the 'Generate Invoice At' (Select) field in DocType
|
||||
#. 'Subscription'
|
||||
@@ -24999,7 +24999,7 @@ msgstr "Ogiltig Artikel Nummer"
|
||||
|
||||
#: erpnext/utilities/transaction_base.py:34
|
||||
msgid "Invalid Posting Time"
|
||||
msgstr "Ogiltig Bokföring Tid"
|
||||
msgstr "Ogiltig Tid"
|
||||
|
||||
#: erpnext/accounts/doctype/party_link/party_link.py:30
|
||||
msgid "Invalid Primary Role"
|
||||
@@ -35283,7 +35283,7 @@ msgstr "Betalning DocType"
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
|
||||
#: erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py:110
|
||||
msgid "Payment Due Date"
|
||||
msgstr "Förfallodatum"
|
||||
msgstr "Förfallo Datum"
|
||||
|
||||
#. Label of the payment_entries (Table) field in DocType 'Bank Clearance'
|
||||
#. Label of the payment_entries (Table) field in DocType 'Bank Transaction'
|
||||
@@ -37826,7 +37826,7 @@ msgstr "Bokföring Datum kan inte vara i framtiden"
|
||||
#: erpnext/stock/doctype/stock_closing_balance/stock_closing_balance.json
|
||||
#: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
|
||||
msgid "Posting Datetime"
|
||||
msgstr "Bokföring Datum och Tid"
|
||||
msgstr "Datum och Tid"
|
||||
|
||||
#. Label of the posting_time (Time) field in DocType 'Dunning'
|
||||
#. Label of the posting_time (Time) field in DocType 'POS Closing Entry'
|
||||
@@ -37870,15 +37870,15 @@ msgstr "Bokföring Datum och Tid"
|
||||
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:41
|
||||
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
|
||||
msgid "Posting Time"
|
||||
msgstr "Bokning Tid"
|
||||
msgstr "Tid"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1852
|
||||
msgid "Posting date and posting time is mandatory"
|
||||
msgstr "Bokföring Datum och Tid erfordras"
|
||||
msgstr "Datum och Tid erfordras"
|
||||
|
||||
#: erpnext/controllers/sales_and_purchase_return.py:54
|
||||
msgid "Posting timestamp must be after {0}"
|
||||
msgstr "Bokning tid måste vara efter {0}"
|
||||
msgstr "Tidsstämpel måste vara efter {0}"
|
||||
|
||||
#. Description of a DocType
|
||||
#: erpnext/crm/doctype/opportunity/opportunity.json
|
||||
|
||||
@@ -740,6 +740,12 @@ class JobCard(Document):
|
||||
bold("Job Card"), get_link_to_form("Job Card", self.name)
|
||||
)
|
||||
)
|
||||
else:
|
||||
for row in self.time_logs:
|
||||
if not row.from_time or not row.to_time:
|
||||
frappe.throw(
|
||||
_("Row #{0}: From Time and To Time fields are required").format(row.idx),
|
||||
)
|
||||
|
||||
precision = self.precision("total_completed_qty")
|
||||
total_completed_qty = flt(
|
||||
|
||||
@@ -1804,6 +1804,7 @@ def get_sub_assembly_items(
|
||||
continue
|
||||
else:
|
||||
stock_qty = stock_qty - _bin_dict.projected_qty
|
||||
sub_assembly_items.append(d.item_code)
|
||||
elif warehouse:
|
||||
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))
|
||||
|
||||
|
||||
@@ -1397,6 +1397,8 @@ class WorkOrder(Document):
|
||||
|
||||
def set_reserved_qty_for_wip_and_fg(self, stock_entry):
|
||||
items = frappe._dict()
|
||||
|
||||
stock_entry.reload()
|
||||
if stock_entry.purpose == "Manufacture" and self.sales_order:
|
||||
items = self.get_finished_goods_for_reservation(stock_entry)
|
||||
elif stock_entry.purpose == "Material Transfer for Manufacture":
|
||||
@@ -1439,7 +1441,10 @@ class WorkOrder(Document):
|
||||
items = frappe._dict()
|
||||
|
||||
so_details = self.get_so_details()
|
||||
qty = so_details.stock_qty - so_details.stock_reserved_qty
|
||||
if not so_details:
|
||||
return items
|
||||
|
||||
qty = so_details.stock_qty - (so_details.stock_reserved_qty + so_details.delivered_qty)
|
||||
if not qty:
|
||||
return items
|
||||
|
||||
@@ -1462,6 +1467,7 @@ class WorkOrder(Document):
|
||||
"from_voucher_no": stock_entry.name,
|
||||
"from_voucher_type": stock_entry.doctype,
|
||||
"from_voucher_detail_no": row.name,
|
||||
"serial_and_batch_bundles": [row.serial_and_batch_bundle],
|
||||
}
|
||||
)
|
||||
else:
|
||||
@@ -1476,9 +1482,8 @@ class WorkOrder(Document):
|
||||
"parent": self.sales_order,
|
||||
"item_code": self.production_item,
|
||||
"docstatus": 1,
|
||||
"stock_reserved_qty": 0,
|
||||
},
|
||||
["name", "stock_qty", "stock_reserved_qty"],
|
||||
["name", "stock_qty", "stock_reserved_qty", "delivered_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -408,3 +408,5 @@ erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes
|
||||
erpnext.patches.v15_0.update_query_report
|
||||
erpnext.patches.v15_0.set_purchase_receipt_row_item_to_capitalization_stock_item
|
||||
erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
|
||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
|
||||
|
||||
20
erpnext/patches/v15_0/rename_group_by_to_categorize_by.py
Normal file
20
erpnext/patches/v15_0/rename_group_by_to_categorize_by.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
rename_field("Process Statement Of Accounts", "group_by", "categorize_by")
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE
|
||||
`tabProcess Statement Of Accounts`
|
||||
SET
|
||||
categorize_by = CASE
|
||||
WHEN categorize_by = 'Group by Voucher (Consolidated)' THEN 'Categorize by Voucher (Consolidated)'
|
||||
WHEN categorize_by = 'Group by Voucher' THEN 'Categorize by Voucher'
|
||||
END
|
||||
WHERE
|
||||
categorize_by IN ('Group by Voucher (Consolidated)', 'Group by Voucher')
|
||||
"""
|
||||
)
|
||||
@@ -500,7 +500,6 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
|
||||
user = frappe.session.user
|
||||
# find customer name from contact.
|
||||
customer = ""
|
||||
timesheets = []
|
||||
|
||||
contact = frappe.db.exists("Contact", {"user": user})
|
||||
if contact:
|
||||
@@ -509,31 +508,43 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20
|
||||
customer = contact.get_link_for("Customer")
|
||||
|
||||
if customer:
|
||||
sales_invoices = [
|
||||
d.name for d in frappe.get_all("Sales Invoice", filters={"customer": customer})
|
||||
] or [None]
|
||||
projects = [d.name for d in frappe.get_all("Project", filters={"customer": customer})]
|
||||
# Return timesheet related data to web portal.
|
||||
timesheets = frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
ts.name, tsd.activity_type, ts.status, ts.total_billable_hours,
|
||||
COALESCE(ts.sales_invoice, tsd.sales_invoice) AS sales_invoice, tsd.project
|
||||
FROM `tabTimesheet` ts, `tabTimesheet Detail` tsd
|
||||
WHERE tsd.parent = ts.name AND
|
||||
(
|
||||
ts.sales_invoice IN %(sales_invoices)s OR
|
||||
tsd.sales_invoice IN %(sales_invoices)s OR
|
||||
tsd.project IN %(projects)s
|
||||
)
|
||||
ORDER BY `end_date` ASC
|
||||
LIMIT {limit_page_length} offset {limit_start}
|
||||
""",
|
||||
dict(sales_invoices=sales_invoices, projects=projects),
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
sales_invoices = frappe.get_all("Sales Invoice", filters={"customer": customer}, pluck="name")
|
||||
projects = frappe.get_all("Project", filters={"customer": customer}, pluck="name")
|
||||
|
||||
return timesheets
|
||||
# Return timesheet related data to web portal.
|
||||
table = frappe.qb.DocType("Timesheet")
|
||||
child_table = frappe.qb.DocType("Timesheet Detail")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.join(child_table)
|
||||
.on(table.name == child_table.parent)
|
||||
.select(
|
||||
table.name,
|
||||
child_table.activity_type,
|
||||
table.status,
|
||||
table.total_billable_hours,
|
||||
(table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"),
|
||||
child_table.project,
|
||||
)
|
||||
.orderby(table.end_date)
|
||||
.limit(limit_page_length)
|
||||
.offset(limit_start)
|
||||
)
|
||||
|
||||
conditions = []
|
||||
if sales_invoices:
|
||||
conditions.extend(
|
||||
[table.sales_invoice.isin(sales_invoices), child_table.sales_invoice.isin(sales_invoices)]
|
||||
)
|
||||
if projects:
|
||||
conditions.append(child_table.project.isin(projects))
|
||||
|
||||
if conditions:
|
||||
query = query.where(frappe.qb.terms.Criterion.any(conditions))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
|
||||
@@ -273,8 +273,6 @@ def install(country=None):
|
||||
{"doctype": "Issue Priority", "name": _("Low")},
|
||||
{"doctype": "Issue Priority", "name": _("Medium")},
|
||||
{"doctype": "Issue Priority", "name": _("High")},
|
||||
{"doctype": "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"},
|
||||
{"doctype": "Email Account", "email_id": "support@example.com", "append_to": "Issue"},
|
||||
{"doctype": "Party Type", "party_type": "Customer", "account_type": "Receivable"},
|
||||
{"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"},
|
||||
{"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"},
|
||||
|
||||
@@ -214,13 +214,12 @@ def get_or_create_account(company_name, account):
|
||||
default_root_type = "Liability"
|
||||
root_type = account.get("root_type", default_root_type)
|
||||
|
||||
or_filters = {"account_name": account.get("account_name")}
|
||||
if account.get("account_number"):
|
||||
or_filters.update({"account_number": account.get("account_number")})
|
||||
|
||||
existing_accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={"company": company_name, "root_type": root_type},
|
||||
or_filters={
|
||||
"account_name": account.get("account_name"),
|
||||
"account_number": account.get("account_number"),
|
||||
},
|
||||
"Account", filters={"company": company_name, "root_type": root_type}, or_filters=or_filters
|
||||
)
|
||||
|
||||
if existing_accounts:
|
||||
|
||||
@@ -7,6 +7,35 @@ const SALES_DOCTYPES = ["Quotation", "Sales Order", "Delivery Note", "Sales Invo
|
||||
const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"];
|
||||
|
||||
frappe.ui.form.on("Item", {
|
||||
valuation_method(frm) {
|
||||
if (!frm.is_new() && frm.doc.valuation_method === "Moving Average") {
|
||||
let stock_exists = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0;
|
||||
let current_valuation_method = frm.doc.__onload.current_valuation_method;
|
||||
|
||||
if (stock_exists && current_valuation_method !== frm.doc.valuation_method) {
|
||||
let msg = __(
|
||||
"Changing the valuation method to Moving Average will affect new transactions. If backdated entries are added, earlier FIFO-based entries will be reposted, which may change closing balances."
|
||||
);
|
||||
msg += "<br>";
|
||||
msg += __(
|
||||
"Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item."
|
||||
);
|
||||
msg += "<br>";
|
||||
msg += __("Do you want to change valuation method?");
|
||||
|
||||
frappe.confirm(
|
||||
msg,
|
||||
() => {
|
||||
frm.set_value("valuation_method", "Moving Average");
|
||||
},
|
||||
() => {
|
||||
frm.set_value("valuation_method", current_valuation_method);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("attribute", "numeric_values", "numeric_values");
|
||||
frm.add_fetch("attribute", "from_range", "from_range");
|
||||
|
||||
@@ -33,6 +33,7 @@ from erpnext.controllers.item_variant import (
|
||||
validate_item_variant_attributes,
|
||||
)
|
||||
from erpnext.stock.doctype.item_default.item_default import ItemDefault
|
||||
from erpnext.stock.utils import get_valuation_method
|
||||
|
||||
|
||||
class DuplicateReorderRows(frappe.ValidationError):
|
||||
@@ -153,6 +154,7 @@ class Item(Document):
|
||||
def onload(self):
|
||||
self.set_onload("stock_exists", self.stock_ledger_created())
|
||||
self.set_onload("asset_naming_series", get_asset_naming_series())
|
||||
self.set_onload("current_valuation_method", get_valuation_method(self.name))
|
||||
|
||||
def autoname(self):
|
||||
if frappe.db.get_default("item_naming_by") == "Naming Series":
|
||||
|
||||
@@ -7,6 +7,8 @@ from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowti
|
||||
from pypika import functions as fn
|
||||
|
||||
import erpnext
|
||||
import erpnext.controllers
|
||||
import erpnext.controllers.status_updater
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
@@ -4178,6 +4180,59 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
||||
|
||||
self.assertTrue(sles)
|
||||
|
||||
def test_internal_pr_qty_change_only_single_batch(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
|
||||
def get_sabb_qty(sabb):
|
||||
return frappe.get_value("Serial and Batch Bundle", sabb, "total_qty")
|
||||
|
||||
item = make_item("Item with only Batch", {"has_batch_no": 1})
|
||||
item.create_new_batch = 1
|
||||
item.save()
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item.item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
company="_Test Company with perpetual inventory",
|
||||
warehouse="Stores - TCP1",
|
||||
)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item.item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
company="_Test Company with perpetual inventory",
|
||||
customer="_Test Internal Customer 2",
|
||||
cost_center="Main - TCP1",
|
||||
warehouse="Stores - TCP1",
|
||||
target_warehouse="Work In Progress - TCP1",
|
||||
)
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
|
||||
pr.items[0].warehouse = "Stores - TCP1"
|
||||
pr.items[0].qty = 8
|
||||
pr.save()
|
||||
|
||||
# Test 1 - Check if SABB qty is changed on first save
|
||||
self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 8)
|
||||
|
||||
pr.items[0].qty = 6
|
||||
pr.items[0].received_qty = 6
|
||||
pr.save()
|
||||
|
||||
# Test 2 - Check if SABB qty is changed when saved again
|
||||
self.assertEqual(abs(get_sabb_qty(pr.items[0].serial_and_batch_bundle)), 6)
|
||||
|
||||
pr.items[0].qty = 12
|
||||
pr.items[0].received_qty = 12
|
||||
|
||||
# Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN
|
||||
self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -203,10 +203,11 @@ class QualityInspection(Document):
|
||||
self.get_item_specification_details()
|
||||
|
||||
def on_update(self):
|
||||
if (
|
||||
frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
|
||||
== "Warn"
|
||||
):
|
||||
action_if_qi_in_draft = frappe.db.get_single_value(
|
||||
"Stock Settings", "action_if_quality_inspection_is_not_submitted"
|
||||
)
|
||||
|
||||
if not action_if_qi_in_draft or action_if_qi_in_draft == "Warn":
|
||||
self.update_qc_reference()
|
||||
|
||||
def on_submit(self):
|
||||
|
||||
@@ -231,6 +231,9 @@ class SerialandBatchBundle(Document):
|
||||
"warehouse": self.warehouse,
|
||||
"check_serial_nos": True,
|
||||
"serial_nos": serial_nos,
|
||||
"sabb_voucher_type": self.voucher_type,
|
||||
"sabb_voucher_no": self.voucher_no,
|
||||
"sabb_voucher_detail_no": self.voucher_detail_no,
|
||||
}
|
||||
if self.voucher_type == "POS Invoice":
|
||||
kwargs["ignore_voucher_nos"] = [self.voucher_no]
|
||||
@@ -1874,9 +1877,16 @@ def get_reserved_serial_nos(kwargs) -> list:
|
||||
ignore_serial_nos.extend(get_reserved_serial_nos_for_pos(kwargs))
|
||||
|
||||
reserved_entries = get_reserved_serial_nos_for_sre(kwargs)
|
||||
if not reserved_entries:
|
||||
return ignore_serial_nos
|
||||
|
||||
reserved_voucher_details = get_reserved_voucher_details(kwargs)
|
||||
|
||||
serial_nos = []
|
||||
for entry in reserved_entries:
|
||||
if entry.voucher_no in reserved_voucher_details:
|
||||
continue
|
||||
|
||||
if kwargs.get("serial_nos") and entry.serial_no in kwargs.get("serial_nos"):
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -1893,6 +1903,29 @@ def get_reserved_serial_nos(kwargs) -> list:
|
||||
return ignore_serial_nos
|
||||
|
||||
|
||||
def get_reserved_voucher_details(kwargs):
|
||||
reserved_voucher_details = []
|
||||
|
||||
value = {
|
||||
"Delivery Note": ["Delivery Note Item", "against_sales_order"],
|
||||
}.get(kwargs.get("voucher_type"))
|
||||
|
||||
if not value or not kwargs.get("sabb_voucher_no"):
|
||||
return reserved_voucher_details
|
||||
|
||||
reserved_voucher_details = frappe.get_all(
|
||||
value[0],
|
||||
pluck=value[1],
|
||||
filters={
|
||||
"name": kwargs.get("sabb_voucher_detail_no"),
|
||||
"parent": kwargs.get("sabb_voucher_no"),
|
||||
"docstatus": 1,
|
||||
},
|
||||
)
|
||||
|
||||
return reserved_voucher_details
|
||||
|
||||
|
||||
def get_reserved_serial_nos_for_pos(kwargs):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_returned_serial_nos
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
@@ -162,7 +162,7 @@ frappe.ui.form.on("Shipment", {
|
||||
args: { contact: contact_name },
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
if (!(r.message.contact_email && (r.message.contact_phone || r.message.contact_mobile))) {
|
||||
if (!(r.message.contact_email || r.message.contact_phone || r.message.contact_mobile)) {
|
||||
if (contact_type == "Delivery") {
|
||||
frm.set_value("delivery_contact_name", "");
|
||||
frm.set_value("delivery_contact", "");
|
||||
|
||||
@@ -462,6 +462,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
docstatus: 1,
|
||||
purpose: "Material Transfer",
|
||||
add_to_transit: 1,
|
||||
per_transferred: ["<", 100],
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -522,17 +522,29 @@ class StockEntry(StockController):
|
||||
).format(frappe.bold(self.company))
|
||||
)
|
||||
|
||||
elif (
|
||||
self.is_opening == "Yes"
|
||||
and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss"
|
||||
):
|
||||
acc_details = frappe.get_cached_value(
|
||||
"Account",
|
||||
d.expense_account,
|
||||
["account_type", "report_type"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if self.is_opening == "Yes" and acc_details.report_type == "Profit and Loss":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry"
|
||||
"Difference Account must be a Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry"
|
||||
),
|
||||
OpeningEntryAccountError,
|
||||
)
|
||||
|
||||
if acc_details.account_type == "Stock":
|
||||
frappe.throw(
|
||||
_(
|
||||
"At row {0}: the Difference Account must not be a Stock type account, please change the Account Type for the account {1} or select a different account"
|
||||
).format(d.idx, get_link_to_form("Account", d.expense_account)),
|
||||
OpeningEntryAccountError,
|
||||
)
|
||||
|
||||
def validate_warehouse(self):
|
||||
"""perform various (sometimes conditional) validations on warehouse"""
|
||||
|
||||
|
||||
@@ -726,6 +726,12 @@ class StockReconciliation(StockController):
|
||||
)
|
||||
|
||||
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
||||
elif self.docstatus == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"No stock ledger entries were created. Please set the quantity or valuation rate for the items properly and try again."
|
||||
)
|
||||
)
|
||||
|
||||
def make_adjustment_entry(self, row, sl_entries):
|
||||
from erpnext.stock.stock_ledger import get_stock_value_difference
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"column_break_6",
|
||||
"warehouse",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"valuation_rate",
|
||||
"amount",
|
||||
"allow_zero_valuation_rate",
|
||||
@@ -86,6 +87,16 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "item_code.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "valuation_rate",
|
||||
@@ -257,7 +268,7 @@
|
||||
"grid_page_length": 50,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-07 10:26:25.856337",
|
||||
"modified": "2025-04-28 22:40:30.086415",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reconciliation Item",
|
||||
@@ -269,4 +280,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class StockReconciliationItem(Document):
|
||||
reconcile_all_serial_batch: DF.Check
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.LongText | None
|
||||
stock_uom: DF.Link | None
|
||||
use_serial_batch_fields: DF.Check
|
||||
valuation_rate: DF.Currency
|
||||
warehouse: DF.Link
|
||||
|
||||
@@ -1093,6 +1093,7 @@ class StockReservation:
|
||||
"Serial and Batch Entry",
|
||||
fields=["serial_no", "batch_no", "qty"],
|
||||
filters={"parent": ("in", serial_batch_bundles)},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
for detail in bundle_details:
|
||||
|
||||
@@ -2109,29 +2109,6 @@ def get_future_sle_with_negative_batch_qty(sle_args):
|
||||
|
||||
|
||||
def validate_reserved_stock(kwargs):
|
||||
if kwargs.serial_no:
|
||||
serial_nos = kwargs.serial_no.split("\n")
|
||||
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
|
||||
|
||||
elif kwargs.batch_no:
|
||||
validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
|
||||
|
||||
elif kwargs.serial_and_batch_bundle:
|
||||
sbb_entries = frappe.db.get_all(
|
||||
"Serial and Batch Entry",
|
||||
{
|
||||
"parenttype": "Serial and Batch Bundle",
|
||||
"parent": kwargs.serial_and_batch_bundle,
|
||||
"docstatus": 1,
|
||||
},
|
||||
["batch_no", "serial_no"],
|
||||
)
|
||||
|
||||
if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
|
||||
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
|
||||
elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
|
||||
validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
|
||||
|
||||
# Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
|
||||
|
||||
@@ -31,8 +31,8 @@ rfq = class rfq {
|
||||
var me = this;
|
||||
$('.rfq-items').on("change", ".rfq-qty", function(){
|
||||
me.idx = parseFloat($(this).attr('data-idx'));
|
||||
me.qty = parseFloat($(this).val()) || 0;
|
||||
me.rate = parseFloat($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val());
|
||||
me.qty = parseFloat(flt($(this).val())) || 0;
|
||||
me.rate = parseFloat(flt($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val()));
|
||||
me.update_qty_rate();
|
||||
$(this).val(format_number(me.qty, doc.number_format, 2));
|
||||
})
|
||||
@@ -42,8 +42,8 @@ rfq = class rfq {
|
||||
var me = this;
|
||||
$(".rfq-items").on("change", ".rfq-rate", function(){
|
||||
me.idx = parseFloat($(this).attr('data-idx'));
|
||||
me.rate = parseFloat($(this).val()) || 0;
|
||||
me.qty = parseFloat($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val());
|
||||
me.rate = parseFloat(flt($(this).val())) || 0;
|
||||
me.qty = parseFloat(flt($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val()));
|
||||
me.update_qty_rate();
|
||||
$(this).val(format_number(me.rate, doc.number_format, 2));
|
||||
})
|
||||
|
||||
@@ -296,6 +296,10 @@ class TransactionBase(StatusUpdater):
|
||||
item_details = self.fetch_item_details(item_obj)
|
||||
|
||||
self.set_fetched_values(item_obj, item_details)
|
||||
|
||||
if self.doctype == "Request for Quotation":
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user