Merge branch 'develop' into unit-price-contract-2

This commit is contained in:
Marica
2025-05-05 15:52:39 +05:30
committed by GitHub
50 changed files with 1237 additions and 850 deletions

View File

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

View File

@@ -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

View File

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

View File

@@ -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:

View File

@@ -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"

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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"):

View File

@@ -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,

View File

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

View File

@@ -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"))

View File

@@ -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,
}
)

View File

@@ -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)
)

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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));
},
});
}

View File

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

View File

@@ -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()

View File

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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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(

View File

@@ -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))

View File

@@ -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,
)

View File

@@ -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")

View 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')
"""
)

View File

@@ -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):

View File

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

View File

@@ -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:

View File

@@ -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");

View File

@@ -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":

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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", "");

View File

@@ -462,6 +462,7 @@ frappe.ui.form.on("Stock Entry", {
docstatus: 1,
purpose: "Material Transfer",
add_to_transit: 1,
per_transferred: ["<", 100],
},
});
},

View File

@@ -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"""

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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));
})

View File

@@ -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)