Merge remote-tracking branch 'upstream/version-15-hotfix' into mergify/bp/version-15-hotfix/pr-49875

This commit is contained in:
barredterra
2025-12-10 13:28:30 +01:00
143 changed files with 28738 additions and 960 deletions

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.82.2"
__version__ = "15.89.2"
def get_default_company(user=None):

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.app") {
} else if (frm.doc.service_provider == "frankfurter.dev") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"options": "frankfurter.dev\nexchangerate.host\nCustom",
"reqd": 1
},
{
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-18 08:32:26.895076",
"modified": "2025-11-25 13:03:41.896424",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -141,8 +141,9 @@
"write": 1
}
],
"sort_field": "modified",
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
disabled: DF.Check
req_params: DF.Table[CurrencyExchangeSettingsDetails]
result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
url: DF.Data | None
use_http: DF.Check
# end: auto-generated types
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider == "frankfurter.app":
elif self.service_provider == "frankfurter.dev":
self.set("result_key", [])
self.set("req_params", [])
@@ -105,11 +105,11 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"
protocol = "https://"
if use_http:

View File

@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
currency=company_currency,
)
if account_details:

View File

@@ -420,7 +420,7 @@ def update_against_account(voucher_type, voucher_no):
if not entries:
return
company_currency = erpnext.get_company_currency(entries[0].company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
accounts_debited, accounts_credited = [], []
for d in entries:

View File

@@ -33,6 +33,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
class StockAccountInvalidTransaction(frappe.ValidationError):
@@ -273,93 +274,7 @@ class JournalEntry(AccountsController):
)
def apply_tax_withholding(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
return
parties = [d.party for d in self.get("accounts") if d.party]
parties = list(set(parties))
if len(parties) > 1:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
account_type_map = get_account_type_map(self.company)
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
debit_or_credit = (
"debit_in_account_currency"
if self.voucher_type == "Credit Note"
else "credit_in_account_currency"
)
rev_debit_or_credit = (
"credit_in_account_currency"
if debit_or_credit == "debit_in_account_currency"
else "debit_in_account_currency"
)
party_account = get_party_account(party_type.title(), parties[0], self.company)
net_total = sum(
d.get(debit_or_credit)
for d in self.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
)
party_amount = sum(
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
)
inv = frappe._dict(
{
party_type: parties[0],
"doctype": doctype,
"company": self.company,
"posting_date": self.posting_date,
"net_total": net_total,
}
)
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
inv, self.tax_withholding_category
)
if not tax_withholding_details:
return
accounts = []
for d in self.get("accounts"):
if d.get("account") == tax_withholding_details.get("account_head"):
d.update(
{
"account": tax_withholding_details.get("account_head"),
debit_or_credit: tax_withholding_details.get("tax_amount"),
}
)
accounts.append(d.get("account"))
if d.get("account") == party_account:
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append(
"accounts",
{
"account": tax_withholding_details.get("account_head"),
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
"against_account": parties[0],
},
)
to_remove = [
d
for d in self.get("accounts")
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
]
for d in to_remove:
self.remove(d)
JournalEntryTaxWithholding(self).apply()
def update_asset_value(self):
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
@@ -1281,6 +1196,230 @@ class JournalEntry(AccountsController):
frappe.throw(_("Accounts table cannot be blank."))
class JournalEntryTaxWithholding:
def __init__(self, journal_entry):
self.doc: JournalEntry = journal_entry
self.party = None
self.party_type = None
self.party_account = None
self.party_row = None
self.existing_tds_rows = []
self.precision = None
self.has_multiple_parties = False
# Direction fields based on party type
self.party_field = None # "credit" for Supplier, "debit" for Customer
self.reverse_field = None # opposite of party_field
def apply(self):
if not self._set_party_info():
return
self._setup_direction_fields()
self._reset_existing_tds()
if not self._should_apply_tds():
self._cleanup_duplicate_tds_rows(None)
return
if self.has_multiple_parties:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
net_total = self._calculate_net_total()
if net_total <= 0:
return
tds_details = self._get_tds_details(net_total)
if not tds_details or not tds_details.get("tax_amount"):
return
self._create_or_update_tds_row(tds_details)
self._update_party_amount(tds_details.get("tax_amount"), is_reversal=False)
self._recalculate_totals()
def _should_apply_tds(self):
return self.doc.apply_tds and self.doc.voucher_type in ("Debit Note", "Credit Note")
def _set_party_info(self):
for row in self.doc.get("accounts"):
if row.party_type in ("Customer", "Supplier") and row.party:
if self.party and row.party != self.party:
self.has_multiple_parties = True
if not self.party:
self.party = row.party
self.party_type = row.party_type
self.party_account = row.account
self.party_row = row
if row.get("is_tax_withholding_account"):
self.existing_tds_rows.append(row)
return bool(self.party)
def _setup_direction_fields(self):
"""
For Supplier (TDS): party has credit, TDS reduces credit
For Customer (TCS): party has debit, TCS increases debit
"""
if self.party_type == "Supplier":
self.party_field = "credit"
self.reverse_field = "debit"
else: # Customer
self.party_field = "debit"
self.reverse_field = "credit"
self.precision = self.doc.precision(self.party_field, self.party_row)
def _reset_existing_tds(self):
for row in self.existing_tds_rows:
# TDS amount is always in credit (liability to government)
tds_amount = flt(row.get("credit") - row.get("debit"), self.precision)
if not tds_amount:
continue
self._update_party_amount(tds_amount, is_reversal=True)
# zero_out_tds_row
row.update(
{
"credit": 0,
"credit_in_account_currency": 0,
"debit": 0,
"debit_in_account_currency": 0,
}
)
def _update_party_amount(self, amount, is_reversal=False):
amount = flt(amount, self.precision)
amount_in_party_currency = flt(amount / self.party_row.get("exchange_rate", 1), self.precision)
# Determine which field the party amount is in
active_field = self.party_field if self.party_row.get(self.party_field) else self.reverse_field
# If amount is in reverse field, flip the signs
if active_field == self.reverse_field:
amount = -amount
amount_in_party_currency = -amount_in_party_currency
# Direction multiplier based on party type:
# Customer (TCS): +1 (add to debit)
# Supplier (TDS): -1 (subtract from credit)
direction = 1 if self.party_type == "Customer" else -1
# Reversal inverts the direction
if is_reversal:
direction = -direction
adjustment = amount * direction
adjustment_in_party_currency = amount_in_party_currency * direction
active_field_account_currency = f"{active_field}_in_account_currency"
self.party_row.update(
{
active_field: flt(self.party_row.get(active_field) + adjustment, self.precision),
active_field_account_currency: flt(
self.party_row.get(active_field_account_currency) + adjustment_in_party_currency,
self.precision,
),
}
)
def _calculate_net_total(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
account_type_map = get_account_type_map(self.doc.company)
return flt(
sum(
d.get(self.reverse_field) - d.get(self.party_field)
for d in self.doc.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
and d.account != self.party_account
and not d.get("is_tax_withholding_account")
),
self.precision,
)
def _get_tds_details(self, net_total):
return get_party_tax_withholding_details(
frappe._dict(
{
"party_type": self.party_type,
"party": self.party,
"doctype": self.doc.doctype,
"company": self.doc.company,
"posting_date": self.doc.posting_date,
"tax_withholding_net_total": net_total,
"base_tax_withholding_net_total": net_total,
"grand_total": net_total,
}
),
self.doc.tax_withholding_category,
)
def _create_or_update_tds_row(self, tds_details):
tax_account = tds_details.get("account_head")
account_currency = get_account_currency(tax_account)
company_currency = frappe.get_cached_value("Company", self.doc.company, "default_currency")
exchange_rate = _get_exchange_rate(account_currency, company_currency, self.doc.posting_date)
tax_amount = flt(tds_details.get("tax_amount"), self.precision)
tax_amount_in_account_currency = flt(tax_amount / exchange_rate, self.precision)
# Find existing TDS row for this account
tax_row = None
for row in self.doc.get("accounts"):
if row.account == tax_account and row.get("is_tax_withholding_account"):
tax_row = row
break
if not tax_row:
tax_row = self.doc.append(
"accounts",
{
"account": tax_account,
"account_currency": account_currency,
"exchange_rate": exchange_rate,
"cost_center": tds_details.get("cost_center"),
"credit": 0,
"credit_in_account_currency": 0,
"debit": 0,
"debit_in_account_currency": 0,
"is_tax_withholding_account": 1,
},
)
# TDS/TCS is always credited (liability to government)
tax_row.update(
{
"credit": tax_amount,
"credit_in_account_currency": tax_amount_in_account_currency,
"debit": 0,
"debit_in_account_currency": 0,
}
)
self._cleanup_duplicate_tds_rows(tax_row)
def _cleanup_duplicate_tds_rows(self, current_tax_row):
rows_to_remove = [
row
for row in self.doc.get("accounts")
if row.get("is_tax_withholding_account") and row != current_tax_row
]
for row in rows_to_remove:
self.doc.remove(row)
def _recalculate_totals(self):
self.doc.set_amounts_in_company_currency()
self.doc.set_total_debit_credit()
self.doc.set_against_account()
@frappe.whitelist()
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
@@ -1649,8 +1788,6 @@ def get_exchange_rate(
credit=None,
exchange_rate=None,
):
from erpnext.setup.utils import get_exchange_rate
account_details = frappe.get_cached_value(
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
)
@@ -1672,8 +1809,8 @@ def get_exchange_rate(
# The date used to retreive the exchange rate here is the date passed
# in as an argument to this function.
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
else:
exchange_rate = 1

View File

@@ -34,6 +34,7 @@
"reference_detail_no",
"advance_voucher_type",
"advance_voucher_no",
"is_tax_withholding_account",
"col_break3",
"is_advance",
"user_remark",
@@ -282,12 +283,19 @@
"options": "advance_voucher_type",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "is_tax_withholding_account",
"fieldtype": "Check",
"label": "Is Tax Withholding Account",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-10-27 13:48:32.805100",
"modified": "2025-11-27 12:23:33.157655",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
debit_in_account_currency: DF.Currency
exchange_rate: DF.Float
is_advance: DF.Literal["No", "Yes"]
is_tax_withholding_account: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -1800,7 +1800,7 @@ class PaymentEntry(AccountsController):
else:
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += tax.base_tax_amount
self.base_total_taxes_and_charges += current_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total

View File

@@ -334,7 +334,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
},
{
fieldtype: "HTML",
options: "<b> New Journal Entry will be posted for the difference amount </b>",
options: __(
"New Journal Entry will be posted for the difference amount. The Posting Date can be modified."
).bold(),
},
],
primary_action: () => {

View File

@@ -765,6 +765,14 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
for inv in dr_cr_notes:
if (
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
< inv.allocated_amount
):
frappe.throw(
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)
)
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
reconcile_dr_or_cr = (

View File

@@ -850,6 +850,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
)
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
doc_updates = {}
for ref in references:
if not ref.payment_request:
@@ -875,7 +876,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
title=_("Invalid Allocated Amount"),
)
# update status
# determine status
if new_outstanding_amount == payment_request["grand_total"]:
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
elif new_outstanding_amount == 0:
@@ -883,12 +884,15 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
elif new_outstanding_amount > 0:
status = "Partially Paid"
# update database
frappe.db.set_value(
"Payment Request",
ref.payment_request,
{"outstanding_amount": new_outstanding_amount, "status": status},
)
# prepare bulk update data
doc_updates[ref.payment_request] = {
"outstanding_amount": new_outstanding_amount,
"status": status,
}
# bulk update all payment requests
if doc_updates:
frappe.db.bulk_update("Payment Request", doc_updates)
def get_dummy_message(doc):

View File

@@ -18,12 +18,17 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.controllers.queries import item_query as _item_query
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.stock_ledger import is_negative_stock_allowed
class PartialPaymentValidationError(frappe.ValidationError):
pass
class ProductBundleStockValidationError(frappe.ValidationError):
pass
class POSInvoice(SalesInvoice):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -350,32 +355,67 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"):
if not d.serial_and_batch_bundle:
available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
if frappe.db.exists("Product Bundle", d.item_code):
(
availability,
is_stock_item,
is_negative_stock_allowed,
) = get_product_bundle_stock_availability(d.item_code, d.warehouse, d.stock_qty)
else:
availability, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
if is_negative_stock_allowed:
continue
item_code, warehouse, _qty = (
frappe.bold(d.item_code),
frappe.bold(d.warehouse),
frappe.bold(d.qty),
)
if is_stock_item and flt(available_stock) <= 0:
frappe.throw(
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
if isinstance(availability, list):
error_msgs = []
for item in availability:
if flt(item["available"]) < flt(item["required"]):
error_msgs.append(
_("<li>Packed Item {0}: Required {1}, Available {2}</li>").format(
frappe.bold(item["item_code"]),
frappe.bold(flt(item["required"], 2)),
frappe.bold(flt(item["available"], 2)),
)
)
if error_msgs:
frappe.throw(
_(
"<b>Row #{0}:</b> Bundle {1} in warehouse {2} has insufficient packed items:<br><div style='margin-top: 15px;'><ul style='line-height: 0.8;'>{3}</ul></div>"
).format(
d.idx,
frappe.bold(d.item_code),
frappe.bold(d.warehouse),
"<br>".join(error_msgs),
),
title=_("Insufficient Stock for Product Bundle Items"),
exc=ProductBundleStockValidationError,
)
else:
item_code, warehouse = frappe.bold(d.item_code), frappe.bold(d.warehouse)
if is_stock_item and flt(availability) <= 0:
frappe.throw(
_("Row #{0}: Item {1} has no stock in warehouse {2}.").format(
d.idx, item_code, warehouse
),
title=_("Item Out of Stock"),
)
elif is_stock_item and flt(availability) < flt(d.stock_qty):
frappe.throw(
_("Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}.").format(
d.idx,
item_code,
warehouse,
frappe.bold(flt(availability, 2)),
frappe.bold(flt(d.stock_qty, 2)),
),
title=_("Insufficient Stock"),
)
def validate_serialised_or_batched_item(self):
error_msg = []
@@ -763,8 +803,6 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
@@ -781,6 +819,26 @@ def get_stock_availability(item_code, warehouse):
return 0, is_stock_item, False
def get_product_bundle_stock_availability(item_code, warehouse, item_qty):
is_stock_item = True
bundle = frappe.get_doc("Product Bundle", item_code)
availabilities = []
for bundle_item in bundle.items:
if frappe.get_value("Item", bundle_item.item_code, "is_stock_item"):
bin_qty = get_bin_qty(bundle_item.item_code, warehouse)
reserved_qty = get_pos_reserved_qty(bundle_item.item_code, warehouse)
available = bin_qty - reserved_qty
availabilities.append(
{
"item_code": bundle_item.item_code,
"required": bundle_item.qty * item_qty,
"available": available,
}
)
return availabilities, is_stock_item, is_negative_stock_allowed(item_code=item_code)
def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc("Product Bundle", bundle_item_code)

View File

@@ -964,6 +964,84 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
frappe.set_user("Administrator")
def test_bundle_stock_availability_validation(self):
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import create_item
init_user_and_profile()
frappe.set_user("Administrator")
warehouse = "_Test Warehouse - _TC"
company = "_Test Company"
# Create stock sub-items
sub_item_a = "_Test Bundle SubA"
if not frappe.db.exists("Item", sub_item_a):
create_item(
item_code=sub_item_a,
is_stock_item=1,
)
sub_item_b = "_Test Bundle SubB"
if not frappe.db.exists("Item", sub_item_b):
create_item(
item_code=sub_item_b,
is_stock_item=1,
)
# Add initial stock: SubA=5, SubB=2
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
bundle_item = "_Test Bundle"
if not frappe.db.exists("Item", bundle_item):
create_item(
item_code=bundle_item,
is_stock_item=0,
)
if not frappe.db.exists("Product Bundle", bundle_item):
make_product_bundle(parent=bundle_item, items=[sub_item_a, sub_item_b])
# Test Case 1: Sufficient stock (bundle qty=1: requires SubA=2 (<=5), SubB=1 (<=2)) -> No error
pos_inv_sufficient = create_pos_invoice(
item=bundle_item,
qty=1,
rate=100,
warehouse=warehouse,
pos_profile=self.pos_profile.name,
do_not_save=1,
)
pos_inv_sufficient.append("payments", {"mode_of_payment": "Cash", "amount": 100, "default": 1})
pos_inv_sufficient.insert()
pos_inv_sufficient.submit()
pos_inv_sufficient.cancel()
pos_inv_sufficient.delete()
# Test Case 2: Insufficient stock (reduce SubB to 1, bundle qty=2: requires SubB=2 >1) -> Error with details
make_stock_entry(item_code=sub_item_b, from_warehouse=warehouse, qty=1, company=company)
pos_inv_insufficient = create_pos_invoice(
item=bundle_item,
qty=2,
rate=100,
warehouse=warehouse,
pos_profile=self.pos_profile.name,
do_not_save=1,
)
pos_inv_insufficient.append("payments", {"mode_of_payment": "Cash", "amount": 200, "default": 1})
pos_inv_insufficient.save()
self.assertRaises(ProductBundleStockValidationError, pos_inv_insufficient.submit)
frappe.set_user("test@example.com")
def create_pos_invoice(**args):
args = frappe._dict(args)

View File

@@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args):
if group_condition:
conditions += " and " + group_condition
if args.get("transaction_date"):
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date", ignore=True
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date")
values["transaction_date"] = date
if args.get("doctype") in [
"Quotation",

View File

@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
@@ -125,8 +126,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
cur_frm.add_custom_button(
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();

View File

@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.selling.SellingController
) {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
this.frm.make_methods = {

View File

@@ -1349,7 +1349,11 @@ class SalesInvoice(SellingController):
)
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
if (
flt(item.base_net_amount, item.precision("base_net_amount"))
or item.is_fixed_asset
or enable_discount_accounting
):
# Do not book income for transfer within same company
if self.is_internal_transfer():
continue

View File

@@ -80,7 +80,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "rate",
"fieldtype": "Int",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -102,7 +102,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -199,7 +199,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "amount",
"fieldtype": "Int",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -221,7 +221,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -324,7 +324,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-01-10 18:32:36.201124",
"modified": "2025-12-10 08:06:40.611761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Share Balance",
@@ -339,4 +339,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View File

@@ -14,7 +14,7 @@ class ShareBalance(Document):
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Int
amount: DF.Currency
current_state: DF.Literal["", "Issued", "Purchased"]
from_no: DF.Int
is_company: DF.Check
@@ -22,7 +22,7 @@ class ShareBalance(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
rate: DF.Int
rate: DF.Currency
share_type: DF.Link
to_no: DF.Int
# end: auto-generated types

View File

@@ -85,6 +85,9 @@ def get_party_details(inv):
if inv.doctype == "Sales Invoice":
party_type = "Customer"
party = inv.customer
elif inv.doctype == "Journal Entry":
party_type = inv.party_type
party = inv.party
else:
party_type = "Supplier"
party = inv.supplier
@@ -155,7 +158,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
party_type, parties, inv, tax_details, posting_date, pan_no
)
if party_type == "Supplier":
if party_type == "Supplier" or inv.doctype == "Journal Entry":
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
@@ -346,7 +349,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
elif party_type == "Customer":
if tax_deducted:
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
tax_amount = 0
if inv.doctype == "Sales Invoice":
tax_amount = 0
else:
tax_amount = inv.base_tax_withholding_net_total * tax_details.rate / 100
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
@@ -718,7 +724,7 @@ def get_advance_adjusted_in_invoice(inv):
def get_invoice_total_without_tcs(inv, tax_details):
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
tcs_tax_row = [d for d in inv.get("taxes") or [] if d.account_head == tax_details.account_head]
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
return inv.grand_total - tcs_tax_row_amount

View File

@@ -848,6 +848,90 @@ class TestTaxWithholdingCategory(FrappeTestCase):
self.assertEqual(payment.taxes[0].tax_amount, 6000)
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
def test_tds_on_journal_entry_for_supplier(self):
"""Test TDS deduction for Supplier in Debit Note"""
frappe.db.set_value(
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS"
)
jv = make_journal_entry_with_tax_withholding(
party_type="Supplier",
party="Test TDS Supplier",
voucher_type="Debit Note",
amount=50000,
save=False,
)
jv.apply_tds = 1
jv.tax_withholding_category = "Cumulative Threshold TDS"
jv.save()
# Again saving should not change tds amount
jv.user_remark = "Test TDS on Journal Entry for Supplier"
jv.save()
jv.submit()
# TDS = 50000 * 10% = 5000
self.assertEqual(len(jv.accounts), 3)
# Find TDS account row
tds_row = None
supplier_row = None
for row in jv.accounts:
if row.account == "TDS - _TC":
tds_row = row
elif row.party == "Test TDS Supplier":
supplier_row = row
self.assertEqual(tds_row.credit, 5000)
self.assertEqual(tds_row.debit, 0)
# Supplier amount should be reduced by TDS
self.assertEqual(supplier_row.credit, 45000)
jv.cancel()
def test_tcs_on_journal_entry_for_customer(self):
"""Test TCS collection for Customer in Credit Note"""
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
# Create Credit Note with amount exceeding threshold
jv = make_journal_entry_with_tax_withholding(
party_type="Customer",
party="Test TCS Customer",
voucher_type="Credit Note",
amount=50000,
save=False,
)
jv.apply_tds = 1
jv.tax_withholding_category = "Cumulative Threshold TCS"
jv.save()
# Again saving should not change tds amount
jv.user_remark = "Test TCS on Journal Entry for Customer"
jv.save()
jv.submit()
# Assert TCS calculation (10% on amount above threshold of 30000)
self.assertEqual(len(jv.accounts), 3)
# Find TCS account row
tcs_row = None
customer_row = None
for row in jv.accounts:
if row.account == "TCS - _TC":
tcs_row = row
elif row.party == "Test TCS Customer":
customer_row = row
# TCS should be credited (liability to government)
self.assertEqual(tcs_row.credit, 2000) # above threshold 20000*10%
self.assertEqual(tcs_row.debit, 0)
# Customer amount should be increased by TCS
self.assertEqual(customer_row.debit, 52000)
jv.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all(
@@ -996,6 +1080,88 @@ def create_payment_entry(**args):
return pe
def make_journal_entry_with_tax_withholding(
party_type,
party,
voucher_type,
amount,
cost_center=None,
posting_date=None,
save=True,
submit=False,
):
"""Helper function to create Journal Entry for tax withholding"""
if not cost_center:
cost_center = "_Test Cost Center - _TC"
jv = frappe.new_doc("Journal Entry")
jv.posting_date = posting_date or today()
jv.company = "_Test Company"
jv.voucher_type = voucher_type
jv.multi_currency = 0
if party_type == "Supplier":
# Debit Note: Expense Dr, Supplier Cr
expense_account = "Stock Received But Not Billed - _TC"
party_account = "Creditors - _TC"
jv.append(
"accounts",
{
"account": expense_account,
"cost_center": cost_center,
"debit_in_account_currency": amount,
"exchange_rate": 1,
},
)
jv.append(
"accounts",
{
"account": party_account,
"party_type": party_type,
"party": party,
"cost_center": cost_center,
"credit_in_account_currency": amount,
"exchange_rate": 1,
},
)
else: # Customer
# Credit Note: Customer Dr, Income Cr
party_account = "Debtors - _TC"
income_account = "Sales - _TC"
jv.append(
"accounts",
{
"account": party_account,
"party_type": party_type,
"party": party,
"cost_center": cost_center,
"debit_in_account_currency": amount,
"exchange_rate": 1,
},
)
jv.append(
"accounts",
{
"account": income_account,
"cost_center": cost_center,
"credit_in_account_currency": amount,
"exchange_rate": 1,
},
)
if save or submit:
jv.insert()
if submit:
jv.submit()
return jv
def create_records():
# create a new suppliers
for name in [

View File

@@ -289,7 +289,9 @@ def merge_similar_entries(gl_map, precision=None):
company_currency = erpnext.get_company_currency(company)
if not precision:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
)
# filter zero debit and credit entries
merged_gl_map = filter(

View File

@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
party_type = self.filters.party_type
doctype = qb.DocType(party_type)
party_details_fields = [
doctype.name.as_("party"),
f"{scrub(party_type)}_name",
f"{scrub(party_type)}_group",
]
if party_type == "Customer":
party_details_fields.append(doctype.territory)
conditions = self.get_party_conditions(doctype)
query = (
qb.from_(doctype)
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
.where(Criterion.all(conditions))
)
query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions))
from frappe.desk.reportview import build_match_conditions
@@ -153,6 +159,31 @@ class PartyLedgerSummaryReport:
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
}
]
columns += [
{
"label": _("Opening Balance"),
@@ -213,35 +244,6 @@ class PartyLedgerSummaryReport:
},
]
# Hidden columns for handling 'User Permissions'
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"hidden": 1,
},
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
"hidden": 1,
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
"hidden": 1,
}
]
return columns
def get_data(self):

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.utils import flt
from pypika import Order
import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
@@ -16,7 +15,7 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i
get_group_by_and_display_fields,
get_tax_accounts,
)
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
from erpnext.accounts.report.utils import get_values_for_columns
def execute(filters=None):
@@ -41,16 +40,6 @@ def _execute(filters=None, additional_table_columns=None):
tax_doctype="Purchase Taxes and Charges",
)
scrubbed_tax_fields = {}
for tax in tax_columns:
scrubbed_tax_fields.update(
{
tax + " Rate": frappe.scrub(tax + " Rate"),
tax + " Amount": frappe.scrub(tax + " Amount"),
}
)
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
data = []
@@ -100,8 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
{
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
f"{tax}_rate": item_tax.get("tax_rate", 0),
f"{tax}_amount": item_tax.get("tax_amount", 0),
}
)
total_tax += flt(item_tax.get("tax_amount"))

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.query_builder import functions as fn
from frappe.utils import cstr, flt
from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
@@ -32,16 +32,6 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
scrubbed_tax_fields = {}
for tax in tax_columns:
scrubbed_tax_fields.update(
{
tax + " Rate": frappe.scrub(tax + " Rate"),
tax + " Amount": frappe.scrub(tax + " Amount"),
}
)
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
@@ -102,8 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
{
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
f"{tax}_rate": item_tax.get("tax_rate", 0),
f"{tax}_amount": item_tax.get("tax_amount", 0),
}
)
if item_tax.get("is_other_charges"):
@@ -546,9 +536,10 @@ def get_tax_accounts(
import json
item_row_map = {}
tax_columns = []
tax_columns = {}
invoice_item_row = {}
itemised_tax = {}
scrubbed_description_map = {}
add_deduct_tax = "charge_type"
tax_amount_precision = (
@@ -605,9 +596,14 @@ def get_tax_accounts(
tax_amount,
) in tax_details:
description = handle_html(description)
if description not in tax_columns and tax_amount:
scrubbed_description = scrubbed_description_map.get(description)
if not scrubbed_description:
scrubbed_description = frappe.scrub(description)
scrubbed_description_map[description] = scrubbed_description
if scrubbed_description not in tax_columns and tax_amount:
# as description is text editor earlier and markup can break the column convention in reports
tax_columns.append(description)
tax_columns[scrubbed_description] = description
if item_wise_tax_detail:
try:
@@ -641,7 +637,7 @@ def get_tax_accounts(
else tax_value
)
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
{
"tax_rate": tax_rate,
"tax_amount": tax_value,
@@ -653,7 +649,7 @@ def get_tax_accounts(
continue
elif charge_type == "Actual" and tax_amount:
for d in invoice_item_row.get(parent, []):
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
{
"tax_rate": "NA",
"tax_amount": flt(
@@ -662,12 +658,14 @@ def get_tax_accounts(
}
)
tax_columns.sort()
for desc in tax_columns:
tax_columns_list = list(tax_columns.keys())
tax_columns_list.sort()
for scrubbed_desc in tax_columns_list:
desc = tax_columns[scrubbed_desc]
columns.append(
{
"label": _(desc + " Rate"),
"fieldname": frappe.scrub(desc + " Rate"),
"fieldname": f"{scrubbed_desc}_rate",
"fieldtype": "Float",
"width": 100,
}
@@ -676,7 +674,7 @@ def get_tax_accounts(
columns.append(
{
"label": _(desc + " Amount"),
"fieldname": frappe.scrub(desc + " Amount"),
"fieldname": f"{scrubbed_desc}_amount",
"fieldtype": "Currency",
"options": "currency",
"width": 100,
@@ -714,7 +712,7 @@ def get_tax_accounts(
},
]
return itemised_tax, tax_columns
return itemised_tax, tax_columns_list
def add_total_row(
@@ -807,5 +805,5 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
total_row["percent_gt"] += item["percent_gt"]
for tax in tax_columns:
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])
total_row.setdefault(f"{tax}_amount", 0.0)
total_row[f"{tax}_amount"] += flt(item[f"{tax}_amount"])

View File

@@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
if (!r.message) {
$(".primary-action").prop("hidden", true);
$(".form-message").text("Capitalize this asset to confirm");
$(".form-message").text(__("Capitalize this asset to confirm"));
frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization");

View File

@@ -1206,7 +1206,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
return {
"company": purchase_doc.company,
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
"purchase_date": purchase_doc.get("posting_date"),
"gross_purchase_amount": flt(first_item.base_net_amount),
"asset_quantity": first_item.qty,
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),

View File

@@ -537,6 +537,7 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.asset_doc = asset
asset_repair.modify_depreciation_schedule()
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)

View File

@@ -139,6 +139,7 @@ class AssetCapitalization(StockController):
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.restore_consumed_asset_items()
self.update_target_asset()
def set_title(self):
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
@@ -607,8 +608,12 @@ class AssetCapitalization(StockController):
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount += total_target_asset_value
asset_doc.purchase_amount += total_target_asset_value
if self.docstatus == 2:
asset_doc.gross_purchase_amount -= total_target_asset_value
asset_doc.purchase_amount -= total_target_asset_value
else:
asset_doc.gross_purchase_amount += total_target_asset_value
asset_doc.purchase_amount += total_target_asset_value
asset_doc.set_status("Work In Progress")
asset_doc.flags.ignore_validate = True
asset_doc.save()

View File

@@ -63,14 +63,7 @@ frappe.ui.form.on("Asset Repair", {
},
refresh: function (frm) {
if (frm.doc.docstatus) {
frm.add_custom_button(__("View General Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
};
frappe.set_route("query-report", "General Ledger");
});
}
frm.events.show_general_ledger(frm);
let sbb_field = frm.get_docfield("stock_items", "serial_and_batch_bundle");
if (sbb_field) {
@@ -134,6 +127,26 @@ frappe.ui.form.on("Asset Repair", {
frm.set_value("repair_cost", 0);
}
},
show_general_ledger: (frm) => {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Accounting Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
__("View")
);
}
},
});
frappe.ui.form.on("Asset Repair Consumed Item", {

View File

@@ -6,6 +6,9 @@ from frappe import _
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
@@ -246,6 +249,12 @@ class AssetRepair(AccountsController):
)
stock_entry.asset_repair = self.name
accounting_dimensions = {
"cost_center": self.cost_center,
"project": self.project,
**{dimension: self.get(dimension) for dimension in get_accounting_dimensions()},
}
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
@@ -257,8 +266,7 @@ class AssetRepair(AccountsController):
"qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate,
"serial_and_batch_bundle": stock_item.serial_and_batch_bundle,
"cost_center": self.cost_center,
"project": self.project,
**accounting_dimensions,
},
)
@@ -315,8 +323,8 @@ class AssetRepair(AccountsController):
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Purchase Invoice",
"against_voucher": self.purchase_invoice,
"against_voucher_type": "Asset",
"against_voucher": self.asset,
"company": self.company,
},
item=self,

View File

@@ -4,6 +4,8 @@
import unittest
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
from erpnext.assets.doctype.asset.asset import (
@@ -294,6 +296,31 @@ class TestAssetRepair(unittest.TestCase):
stock_entry = frappe.get_last_doc("Stock Entry")
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
def test_gl_entries_with_capitalized_asset_repair(self):
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
asset_repair = create_asset_repair(
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
)
asset.reload()
GLEntry = qb.DocType("GL Entry")
res = (
qb.from_(GLEntry)
.select(Sum(GLEntry.debit_in_account_currency).as_("total_debit"))
.where(
(GLEntry.voucher_type == "Asset Repair")
& (GLEntry.voucher_no == asset_repair.name)
& (GLEntry.against_voucher_type == "Asset")
& (GLEntry.against_voucher == asset.name)
& (GLEntry.company == asset.company)
& (GLEntry.is_cancelled == 0)
)
).run(as_dict=True)
booked_value = res[0].total_debit if res else 0
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
self.assertEqual(booked_value, asset_repair.repair_cost)
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations

View File

@@ -36,6 +36,7 @@
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
"over_transfer_allowance",
"validate_consumed_qty",
"section_break_xcug",
"auto_create_subcontracting_order",
"column_break_izrr",
@@ -270,6 +271,14 @@
"label": "Fixed Outgoing Email Account",
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
"options": "Email Account"
},
{
"default": "0",
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
}
],
"grid_page_length": 50,
@@ -278,7 +287,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-08-20 22:13:38.506889",
"modified": "2025-11-20 12:59:09.925862",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -44,6 +44,7 @@ class BuyingSettings(Document):
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
supplier_group: DF.Link | None
use_transaction_date_exchange_rate: DF.Check
validate_consumed_qty: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -303,6 +303,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
erpnext.buying.BuyingController
) {
setup() {
this.setup_accounting_dimension_triggers();
this.frm.custom_make_buttons = {
"Purchase Receipt": "Purchase Receipt",
"Purchase Invoice": "Purchase Invoice",

View File

@@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", {
frm.set_query("supplier_primary_contact", function (doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact",
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: {
supplier: doc.name,
type: "Contact",
},
};
});
frm.set_query("supplier_primary_address", function (doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: {
link_doctype: "Supplier",
link_name: doc.name,
supplier: doc.name,
type: "Address",
},
};
});
@@ -137,6 +139,14 @@ frappe.ui.form.on("Supplier", {
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
frm.set_query("supplier_group", () => {
return {
filters: {
is_group: 0,
},
};
});
},
get_supplier_group_details: function (frm) {
frappe.call({

View File

@@ -215,19 +215,25 @@ class Supplier(TransactionBase):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier")
contact = frappe.qb.DocType("Contact")
type = filters.get("type")
type_doctype = frappe.qb.DocType(type)
dynamic_link = frappe.qb.DocType("Dynamic Link")
return (
frappe.qb.from_(contact)
query = (
frappe.qb.from_(type_doctype)
.join(dynamic_link)
.on(contact.name == dynamic_link.parent)
.select(contact.name, contact.email_id)
.on(type_doctype.name == dynamic_link.parent)
.select(type_doctype.name)
.where(
(dynamic_link.link_name == supplier)
& (dynamic_link.link_doctype == "Supplier")
& (contact.name.like(f"%{txt}%"))
& (type_doctype.name.like(f"%{txt}%"))
)
).run(as_dict=False)
)
if type == "Contact":
query = query.select(type_doctype.email_id)
return query.run()

View File

@@ -308,6 +308,31 @@ class AccountsController(TransactionBase):
self.set_default_letter_head()
self.validate_company_in_accounting_dimension()
self.validate_party_address_and_contact()
self.validate_company_linked_addresses()
def validate_company_linked_addresses(self):
address_fields = []
if self.doctype in ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
address_fields = ["dispatch_address_name", "company_address"]
elif self.doctype in ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"):
address_fields = ["billing_address", "shipping_address"]
for field in address_fields:
address = self.get(field)
if address and not frappe.db.exists(
"Dynamic Link",
{
"parent": address,
"parenttype": "Address",
"link_doctype": "Company",
"link_name": self.company,
},
):
frappe.throw(
_("{0} does not belong to the {1}.").format(
_(self.meta.get_label(field)), bold(self.company)
)
)
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
@@ -362,6 +387,24 @@ class AccountsController(TransactionBase):
for _doctype in repost_doctypes:
dt = frappe.qb.DocType(_doctype)
cancelled_entries = (
frappe.qb.from_(dt)
.select(dt.parent, dt.parenttype)
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name) & (dt.docstatus == 2))
.run(as_dict=True)
)
if cancelled_entries:
entries = "<br>".join([get_link_to_form(d.parenttype, d.parent) for d in cancelled_entries])
frappe.throw(
_(
"The following cancelled repost entries exist for <b>{0}</b>:<br><br>{1}<br><br>"
"Kindly delete these entries before continuing."
).format(self.name, entries)
)
rows = (
frappe.qb.from_(dt)
.select(dt.name, dt.parent, dt.parenttype)

View File

@@ -185,7 +185,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
frappe.get_meta(doc.doctype + " Item").get_field(
"stock_qty" if doc.get("update_stock", "") else "qty"
),
company_currency,
currency=company_currency,
)
for column in fields:

View File

@@ -86,6 +86,7 @@ status_map = {
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],

View File

@@ -11,6 +11,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
get_available_serial_nos,
@@ -505,7 +506,7 @@ class SubcontractingController(StockController):
if item.get("serial_and_batch_bundle"):
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
@@ -686,7 +687,11 @@ class SubcontractingController(StockController):
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
row.serial_no = "\n".join(serial_nos)
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
elif (
item_details.has_batch_no
and not row.serial_and_batch_bundle
and (not row.batch_no or self.batch_has_not_available(row.batch_no, row.consumed_qty))
):
batches = get_auto_batch_nos(kwargs)
if batches:
consumed_qty = row.consumed_qty
@@ -711,6 +716,11 @@ class SubcontractingController(StockController):
)
consumed_qty -= d.get("qty")
def batch_has_not_available(self, batch_no, qty_required):
batch_qty = get_batch_qty(batch_no, self.supplier_warehouse, consider_negative_batches=True)
return batch_qty < qty_required
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":
return
@@ -849,7 +859,7 @@ class SubcontractingController(StockController):
if self.doctype == self.subcontract_data.order_doctype or (
self.backflush_based_on == "BOM" or self.is_return
):
for bom_item in self.__get_materials_from_bom(
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.query_builder import functions
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from frappe.utils.deprecations import deprecated
@@ -685,6 +686,22 @@ class calculate_taxes_and_totals:
discount_amount = self.doc.discount_amount or 0
grand_total = self.doc.grand_total
if self.doc.get("is_return") and self.doc.get("return_against"):
doctype = frappe.qb.DocType(self.doc.doctype)
result = (
frappe.qb.from_(doctype)
.select(functions.Sum(doctype.discount_amount).as_("total_return_discount"))
.where(
(doctype.return_against == self.doc.return_against)
& (doctype.is_return == 1)
& (doctype.docstatus == 1)
)
).run(as_dict=True)
total_return_discount = abs(result[0].get("total_return_discount") or 0)
discount_amount += total_return_discount
# validate that discount amount cannot exceed the total before discount
if (
(grand_total >= 0 and discount_amount > grand_total)

View File

@@ -16,7 +16,10 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_purchase_order,
prepare_data_for_internal_transfer,
)
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@@ -2428,3 +2431,34 @@ class TestAccountsController(FrappeTestCase):
# Second return should only get remaining discount (100 - 60 = 40)
self.assertEqual(return_si_2.discount_amount, -40)
def test_company_linked_address(self):
from erpnext.crm.doctype.prospect.test_prospect import make_address
company_address = make_address(
address_title="Company", address_type="Shipping", address_line1="100", city="Mumbai"
)
company_address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
company_address.save()
customer_shipping = make_address(
address_title="Customer", address_type="Shipping", address_line1="10"
)
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_shipping.save()
supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad")
supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_billing.save()
po = create_purchase_order(do_not_save=True)
po.shipping_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, po.save)
po.shipping_address = company_address.name
po.save()
po.billing_address = supplier_billing.name
self.assertRaises(frappe.ValidationError, po.save)
po.billing_address = company_address.name
po.reload()
po.save()

View File

@@ -123,7 +123,7 @@ def send_mail(entry, email_campaign):
subject=frappe.render_template(email_template.get("subject"), context),
content=frappe.render_template(email_template.response_, context),
sender=sender,
recipients=recipient_list,
bcc=recipient_list,
communication_medium="Email",
sent_or_received="Sent",
send_email=True,

View File

@@ -340,10 +340,7 @@ doc_events = {
"User": {
"after_insert": "frappe.contacts.doctype.contact.contact.update_contact",
"validate": "erpnext.setup.doctype.employee.employee.validate_employee_role",
"on_update": [
"erpnext.setup.doctype.employee.employee.update_user_permissions",
"erpnext.portal.utils.set_default_role",
],
"on_update": "erpnext.portal.utils.set_default_role",
},
"Communication": {
"on_update": [
@@ -415,29 +412,29 @@ scheduler_events = {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"0/30 * * * *": [],
# Hourly but offset by 30 minutes
"30 * * * *": [
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
],
# Daily but offset by 45 minutes
"45 0 * * *": [
"erpnext.stock.reorder_item.reorder_item",
],
"45 0 * * *": [],
},
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
],
"hourly_long": [
"hourly_long": [],
"hourly_maintenance": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.utilities.bulk_transaction.retry",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"daily": [
"daily": [],
"daily_long": [],
"daily_maintenance": [
"erpnext.support.doctype.issue.issue.auto_close_tickets",
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
"erpnext.controllers.accounts_controller.update_invoice_status",
@@ -461,17 +458,16 @@ scheduler_events = {
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
"erpnext.accounts.utils.run_ledger_health_checks",
"erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"daily_long": [
"erpnext.stock.reorder_item.reorder_item",
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",

View File

@@ -389,10 +389,12 @@ frappe.ui.form.on("BOM", {
);
has_template_rm.forEach((d) => {
let bom_qty = dialog.fields_dict.qty?.value || 1;
dialog.fields_dict.items.df.data.push({
item_code: d.item_code,
variant_item_code: "",
qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1),
qty: flt(d.qty / frm.doc.quantity) * flt(bom_qty),
source_warehouse: d.source_warehouse,
operation: d.operation,
});

View File

@@ -801,7 +801,9 @@ class BOM(WebsiteGenerator):
)
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount")
)
d.base_amount = d.amount * flt(self.conversion_rate)
d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
self.quantity, self.precision("quantity")
@@ -824,7 +826,10 @@ class BOM(WebsiteGenerator):
d.base_rate = flt(d.rate, d.precision("rate")) * flt(
self.conversion_rate, self.precision("conversion_rate")
)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty"))
d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")),
d.precision("amount"),
)
d.base_amount = flt(d.amount, d.precision("amount")) * flt(
self.conversion_rate, self.precision("conversion_rate")
)

View File

@@ -178,17 +178,12 @@ class JobCard(Document):
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
msg = f"""
Qty To Manufacture in the job card
cannot be greater than Qty To Manufacture in the
work order for the operation {bold(self.operation)}.
<br><br><b>Solution: </b> Either you can reduce the
Qty To Manufacture in the job card or set the
'Overproduction Percentage For Work Order'
in the {form_link}."""
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
frappe.throw(
_(
"Qty To Manufacture in the job card cannot be greater than Qty To Manufacture in the work order for the operation {0}. <br><br><b>Solution: </b> Either you can reduce the Qty To Manufacture in the job card or set the 'Overproduction Percentage For Work Order' in the {1}."
).format(bold(self.operation), form_link),
title=_("Extra Job Card Quantity"),
)
def set_sub_operations(self):
if not self.sub_operations and self.operation:
@@ -605,7 +600,7 @@ class JobCard(Document):
op_row.employee.append(time_log.employee)
if time_log.time_in_mins:
op_row.completed_time += time_log.time_in_mins
op_row.completed_qty += time_log.completed_qty
op_row.completed_qty += flt(time_log.completed_qty)
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
@@ -1064,14 +1059,16 @@ class JobCard(Document):
)
if row.completed_qty < current_operation_qty:
msg = f"""The completed quantity {bold(current_operation_qty)}
of an operation {bold(self.operation)} cannot be greater
than the completed quantity {bold(row.completed_qty)}
of a previous operation
{bold(row.operation)}.
"""
frappe.throw(_(msg))
frappe.throw(
_(
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
).format(
bold(current_operation_qty),
bold(self.operation),
bold(row.completed_qty),
bold(row.operation),
)
)
def validate_work_order(self):
if self.is_work_order_closed():

View File

@@ -1627,7 +1627,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
"min_order_qty": item_master.min_order_qty,
"default_material_request_type": item_master.default_material_request_type,
"qty": planned_qty or 1,
"is_sub_contracted": item_master.is_subcontracted_item,
"is_sub_contracted": item_master.is_sub_contracted_item,
"item_code": item_master.name,
"description": item_master.description,
"stock_uom": item_master.stock_uom,

View File

@@ -209,7 +209,7 @@ frappe.ui.form.on("Work Order", {
if (not_completed && not_completed.length) {
frm.add_custom_button(__("Create Job Card"), () => {
frm.trigger("make_job_card");
}).addClass("btn-primary");
});
}
}
}
@@ -229,7 +229,8 @@ frappe.ui.form.on("Work Order", {
if (
frm.doc.docstatus === 1 &&
["Closed", "Completed"].includes(frm.doc.status) &&
frm.doc.produced_qty > 0
frm.doc.produced_qty > 0 &&
frm.doc.produced_qty > frm.doc.disassembled_qty
) {
frm.add_custom_button(
__("Disassemble Order"),
@@ -253,7 +254,7 @@ frappe.ui.form.on("Work Order", {
if (non_consumed_items && non_consumed_items.length) {
frm.add_custom_button(__("Return Components"), function () {
frm.trigger("create_stock_return_entry");
}).addClass("btn-primary");
});
}
}
},
@@ -406,7 +407,6 @@ frappe.ui.form.on("Work Order", {
work_order_id: frm.doc.name,
purpose: "Disassemble",
qty: data.qty,
target_warehouse: data.target_warehouse,
});
})
.then((stock_entry) => {
@@ -863,24 +863,6 @@ erpnext.work_order = {
},
];
if (purpose === "Disassemble") {
fields.push({
fieldtype: "Link",
options: "Warehouse",
fieldname: "target_warehouse",
label: __("Target Warehouse"),
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
get_query() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
},
};
},
});
}
return new Promise((resolve, reject) => {
frm.qty_prompt = frappe.prompt(
fields,

View File

@@ -979,14 +979,14 @@ class WorkOrder(Document):
for d in self.get("operations"):
precision = d.precision("completed_qty")
qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision)
qty = flt(flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision), precision)
if not qty:
d.status = "Pending"
elif flt(qty) < flt(self.qty):
elif qty < flt(self.qty, precision):
d.status = "Work in Progress"
elif flt(qty) == flt(self.qty):
elif qty == flt(self.qty, precision):
d.status = "Completed"
elif flt(qty) <= max_allowed_qty_for_wo:
elif qty <= flt(max_allowed_qty_for_wo, precision):
d.status = "Completed"
else:
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
@@ -1373,6 +1373,13 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
item_details = get_item_details(item, project)
if frappe.db.get_value("Item", item, "variant_of"):
if variant_bom := frappe.db.get_value(
"BOM",
{"item": item, "is_default": 1, "docstatus": 1},
):
bom_no = variant_bom
wo_doc = frappe.new_doc("Work Order")
wo_doc.production_item = item
wo_doc.update(item_details)

View File

@@ -318,7 +318,7 @@
"type": "Link"
}
],
"modified": "2024-10-21 14:13:38.777556",
"modified": "2025-11-24 11:11:28.343568",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -336,7 +336,7 @@
"doc_view": "List",
"label": "Learn Manufacturing",
"type": "URL",
"url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app"
"url": "https://school.frappe.io/lms/courses/production-planning-and-execution"
},
{
"color": "Grey",

View File

@@ -425,4 +425,5 @@ erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
erpnext.patches.v15_0.set_asset_status_if_not_already_set
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter

View File

@@ -0,0 +1,12 @@
import frappe
def execute():
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
return
settings.service_provider = "frankfurter.dev"
settings.set_parameters_and_result()
settings.flags.ignore_validate = True
settings.save()

View File

@@ -1093,6 +1093,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.refresh_field("payment_schedule");
}
cost_center(doc) {
this.frm.doc.items.forEach((item) => {
item.cost_center = doc.cost_center;
});
this.frm.refresh_field("items");
}
due_date(doc, cdt, cdn) {
// due_date is to be changed, payment terms template and/or payment schedule must
// be removed as due_date is automatically changed based on payment terms
@@ -2749,6 +2757,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
]);
}
}
setup_accounting_dimension_triggers() {
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
callback: function (r) {
if (r.message && r.message[0]) {
let dimensions = r.message[0].map((d) => d.fieldname);
dimensions.forEach((dim) => {
// nosemgrep: frappe-semgrep-rules.rules.frappe-cur-frm-usage
cur_frm.cscript[dim] = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", dim);
};
});
}
},
});
}
};
erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) {

View File

@@ -138,15 +138,15 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() =>
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
this.show_scan_message(row.idx, !is_new_row, qty);
}),
() => this.set_barcode_uom(row, uom),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode),
() => this.set_warehouse(row),
() =>
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
this.show_scan_message(row.idx, !is_new_row, qty);
}),
() => this.clean_up(),
() => this.revert_selector_flag(),
() => resolve(row),

View File

@@ -55,17 +55,20 @@ frappe.ui.form.on("Customer", {
frm.set_query("customer_primary_contact", function (doc) {
return {
query: "erpnext.selling.doctype.customer.customer.get_customer_primary_contact",
query: "erpnext.selling.doctype.customer.customer.get_customer_primary",
filters: {
customer: doc.name,
type: "Contact",
},
};
});
frm.set_query("customer_primary_address", function (doc) {
return {
query: "erpnext.selling.doctype.customer.customer.get_customer_primary",
filters: {
link_doctype: "Customer",
link_name: doc.name,
customer: doc.name,
type: "Address",
},
};
});
@@ -197,6 +200,22 @@ frappe.ui.form.on("Customer", {
var grid = cur_frm.get_field("sales_team").grid;
grid.set_column_disp("allocated_amount", false);
grid.set_column_disp("incentives", false);
frm.set_query("customer_group", () => {
return {
filters: {
is_group: 0,
},
};
});
frm.set_query("territory", () => {
return {
filters: {
is_group: 0,
},
};
});
},
validate: function (frm) {
if (frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name);

View File

@@ -610,7 +610,7 @@
"link_fieldname": "party"
}
],
"modified": "2025-03-05 10:01:47.885574",
"modified": "2025-11-25 09:35:56.772949",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
@@ -696,4 +696,4 @@
"states": [],
"title_field": "customer_name",
"track_changes": 1
}
}

View File

@@ -232,7 +232,7 @@ class Customer(TransactionBase):
self.update_lead_status()
if self.flags.is_new_doc:
self.link_lead_address_and_contact()
self.link_address_and_contact()
self.copy_communication()
self.update_customer_groups()
@@ -272,15 +272,23 @@ class Customer(TransactionBase):
if self.lead_name:
frappe.db.set_value("Lead", self.lead_name, "status", "Converted")
def link_lead_address_and_contact(self):
if self.lead_name:
# assign lead address and contact to customer (if already not set)
def link_address_and_contact(self):
linked_documents = {
"Lead": self.lead_name,
"Opportunity": self.opportunity_name,
"Prospect": self.prospect_name,
}
for doctype, docname in linked_documents.items():
# assign lead, opportunity and prospect address and contact to customer (if already not set)
if not docname:
continue
linked_contacts_and_addresses = frappe.get_all(
"Dynamic Link",
filters=[
["parenttype", "in", ["Contact", "Address"]],
["link_doctype", "=", "Lead"],
["link_name", "=", self.lead_name],
["link_doctype", "=", doctype],
["link_name", "=", docname],
],
fields=["parent as name", "parenttype as doctype"],
)
@@ -792,21 +800,29 @@ def make_address(args, is_primary_address=1, is_shipping_address=1):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters):
def get_customer_primary(doctype, txt, searchfield, start, page_len, filters):
customer = filters.get("customer")
con = qb.DocType("Contact")
type = filters.get("type")
type_doctype = qb.DocType(type)
dlink = qb.DocType("Dynamic Link")
return (
qb.from_(con)
query = (
qb.from_(type_doctype)
.join(dlink)
.on(con.name == dlink.parent)
.select(con.name, con.email_id)
.where((dlink.link_name == customer) & (con.name.like(f"%{txt}%")))
.run()
.on(type_doctype.name == dlink.parent)
.select(type_doctype.name)
.where(
(dlink.link_name == customer)
& (type_doctype.name.like(f"%{txt}%"))
& (dlink.link_doctype == "Customer")
)
)
if type == "Contact":
query = query.select(type_doctype.email_id)
return query.run()
def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]:
"""Parse full name into first name, middle name and last name"""

View File

@@ -574,6 +574,10 @@ frappe.ui.form.on("Sales Order Item", {
});
erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController {
setup(doc) {
this.setup_accounting_dimension_triggers();
super.setup(doc);
}
onload(doc, dt, dn) {
super.onload(doc, dt, dn);
}

View File

@@ -5,6 +5,7 @@
import json
import frappe
from frappe.query_builder import DocType, Order
from frappe.utils import cint
from frappe.utils.nestedset import get_root_of
@@ -200,18 +201,24 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
for item in items_data:
item.actual_qty, _, is_negative_stock_allowed = get_stock_availability(item.item_code, warehouse)
item_prices = frappe.get_all(
"Item Price",
fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"],
filters={
"price_list": price_list,
"item_code": item.item_code,
"selling": True,
"valid_from": ["<=", current_date],
"valid_upto": ["in", [None, "", current_date]],
},
order_by="valid_from desc",
)
ItemPrice = DocType("Item Price")
item_prices = (
frappe.qb.from_(ItemPrice)
.select(
ItemPrice.price_list_rate,
ItemPrice.currency,
ItemPrice.uom,
ItemPrice.batch_no,
ItemPrice.valid_from,
ItemPrice.valid_upto,
)
.where(ItemPrice.price_list == price_list)
.where(ItemPrice.item_code == item.item_code)
.where(ItemPrice.selling == 1)
.where((ItemPrice.valid_from <= current_date) | (ItemPrice.valid_from.isnull()))
.where((ItemPrice.valid_upto >= current_date) | (ItemPrice.valid_upto.isnull()))
.orderby(ItemPrice.valid_from, order=Order.desc)
).run(as_dict=True)
stock_uom_price = next((d for d in item_prices if d.get("uom") == item.stock_uom), {})
item_uom = item.stock_uom

View File

@@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs):
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
if test_exchange_values.get(kwargs["params"]["date"]):
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"):
elif args[0].startswith("https://api.frankfurter.dev") and kwargs.get("params"):
if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
date = args[0].replace("https://api.frankfurter.app/", "")
date = args[0].replace("https://api.frankfurter.dev/v1/", "")
if test_exchange_values.get(date):
return PatchResponse(
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200
@@ -149,7 +149,7 @@ class TestCurrencyExchange(unittest.TestCase):
self.assertEqual(flt(exchange_rate, 3), 65.1)
settings = frappe.get_single("Currency Exchange Settings")
settings.service_provider = "frankfurter.app"
settings.service_provider = "frankfurter.dev"
settings.save()
def test_exchange_rate_strict(self, mock_get):

View File

@@ -139,7 +139,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2023-06-02 13:40:34.435822",
"modified": "2025-12-02 13:58:03.378607",
"modified_by": "Administrator",
"module": "Setup",
"name": "Customer Group",
@@ -208,9 +208,11 @@
"share": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_customer_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
"states": [],
"translated_doctype": 1
}

View File

@@ -10,6 +10,7 @@ from frappe.permissions import (
remove_user_permission,
)
from frappe.utils import cstr, getdate, today, validate_email_address
from frappe.utils.deprecations import deprecated
from frappe.utils.nestedset import NestedSet
from erpnext.utilities.transaction_base import delete_events
@@ -88,9 +89,6 @@ class Employee(NestedSet):
if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"):
return
if not has_permission("User Permission", ptype="write", raise_exception=False):
return
employee_user_permission_exists = frappe.db.exists(
"User Permission", {"allow": "Employee", "for_value": self.name, "user": self.user_id}
)
@@ -251,8 +249,9 @@ def validate_employee_role(doc, method=None, ignore_emp_check=False):
doc.get("roles").remove(doc.get("roles", {"role": "Employee Self Service"})[0])
@deprecated
def update_user_permissions(doc, method):
# called via User hook
# formerly called via User hook
if "Employee" in [d.role for d in doc.get("roles")]:
if not has_permission("User Permission", ptype="write", raise_exception=False):
return

View File

@@ -135,7 +135,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
"modified": "2024-01-30 14:08:38.485616",
"modified": "2025-12-02 13:58:30.018021",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
@@ -202,9 +202,11 @@
"share": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_item_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
"states": [],
"translated_doctype": 1
}

View File

@@ -41,6 +41,7 @@
{
"bold": 1,
"default": "0",
"description": "Only leaf nodes are allowed in transaction",
"fieldname": "is_group",
"fieldtype": "Check",
"in_list_view": 1,
@@ -107,7 +108,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2022-12-24 11:16:12.486719",
"modified": "2025-12-02 13:57:57.814686",
"modified_by": "Administrator",
"module": "Setup",
"name": "Supplier Group",
@@ -168,8 +169,10 @@
"share": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}
"states": [],
"translated_doctype": 1
}

View File

@@ -46,6 +46,7 @@
{
"bold": 1,
"default": "0",
"description": "Only leaf nodes are allowed in transaction",
"fieldname": "is_group",
"fieldtype": "Check",
"in_list_view": 1,
@@ -123,7 +124,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2022-12-24 11:16:39.964956",
"modified": "2025-12-02 13:58:55.190485",
"modified_by": "Administrator",
"module": "Setup",
"name": "Territory",
@@ -186,9 +187,11 @@
"share": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_territory,territory_manager",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
"states": [],
"translated_doctype": 1
}

View File

@@ -92,7 +92,7 @@ def setup_currency_exchange():
ces.set("result_key", [])
ces.set("req_params", [])
ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}"
ces.api_endpoint = "https://api.frankfurter.dev/v1/{transaction_date}"
ces.append("result_key", {"key": "rates"})
ces.append("result_key", {"key": "{to_currency}"})
ces.append("req_params", {"key": "base", "value": "{from_currency}"})

View File

@@ -140,6 +140,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
erpnext.selling.SellingController
) {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
this.frm.make_methods = {

View File

@@ -1091,7 +1091,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed",
"options": "\nDraft\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
@@ -1404,7 +1404,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2025-08-04 19:20:47.724218",
"modified": "2025-12-02 23:55:25.415443",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -126,7 +126,9 @@ class DeliveryNote(SellingController):
shipping_address_name: DF.Link | None
shipping_rule: DF.Link | None
source: DF.Link | None
status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"]
status: DF.Literal[
"", "Draft", "To Bill", "Completed", "Return", "Return Issued", "Cancelled", "Closed"
]
tax_category: DF.Link | None
tax_id: DF.Data | None
taxes: DF.Table[SalesTaxesandCharges]

View File

@@ -12,8 +12,8 @@ frappe.listview_settings["Delivery Note"] = {
"currency",
],
get_indicator: function (doc) {
if (cint(doc.is_return) == 1) {
return [__("Return"), "gray", "is_return,=,Yes"];
if (cint(doc.is_return) == 1 && doc.status == "Return") {
return [__("Return"), "gray", "is_return,=,1"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
} else if (doc.status === "Return Issued") {

View File

@@ -2581,6 +2581,7 @@ class TestDeliveryNote(FrappeTestCase):
dn = make_delivery_note(so.name)
dn.submit()
self.assertEqual(dn.per_billed, 0)
self.assertEqual(dn.status, "To Bill")
si = make_sales_invoice(dn.name)
si.location = "Test Location"
@@ -2595,6 +2596,7 @@ class TestDeliveryNote(FrappeTestCase):
dn.load_from_db()
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.per_returned, 100)
self.assertEqual(returned.status, "Return")
def test_sales_return_for_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle

View File

@@ -196,8 +196,7 @@ class InventoryDimension(Document):
options=self.reference_document,
label=_("Rejected " + self.dimension_name),
search_index=1,
reqd=self.reqd,
mandatory_depends_on=self.mandatory_depends_on,
mandatory_depends_on="eval:doc.rejected_qty > 0",
)
)

View File

@@ -224,8 +224,15 @@ frappe.ui.form.on("Item", {
["is_stock_item", "has_serial_no", "has_batch_no", "has_variants"].forEach((fieldname) => {
frm.set_df_property(fieldname, "read_only", stock_exists);
});
frm.set_df_property("is_fixed_asset", "read_only", frm.doc.__onload?.asset_exists ? 1 : 0);
frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0);
frm.set_query("item_group", () => {
return {
filters: {
is_group: 0,
},
};
});
},
validate: function (frm) {

View File

@@ -243,8 +243,7 @@
"default": "0",
"fieldname": "is_fixed_asset",
"fieldtype": "Check",
"label": "Is Fixed Asset",
"set_only_once": 1
"label": "Is Fixed Asset"
},
{
"allow_in_quick_entry": 1,
@@ -895,7 +894,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2025-08-08 14:58:48.674193",
"modified": "2025-12-04 09:11:56.029567",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -155,6 +155,7 @@ class Item(Document):
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))
self.set_onload("asset_exists", self.has_submitted_assets())
def autoname(self):
if frappe.db.get_default("item_naming_by") == "Naming Series":
@@ -306,9 +307,8 @@ class Item(Document):
if self.stock_ledger_created():
frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created."))
if not self.is_fixed_asset:
asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1)
if asset:
if not self.is_fixed_asset and not self.is_new():
if self.has_submitted_assets():
frappe.throw(
_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')
)
@@ -525,6 +525,9 @@ class Item(Document):
)
return self._stock_ledger_created
def has_submitted_assets(self):
return bool(frappe.db.exists("Asset", {"item_code": self.name, "docstatus": 1}))
def update_item_price(self):
frappe.db.sql(
"""

View File

@@ -96,6 +96,7 @@ frappe.ui.form.on("Material Request", {
refresh: function (frm) {
frm.events.make_custom_buttons(frm);
frm.toggle_reqd("customer", frm.doc.material_request_type == "Customer Provided");
frm.trigger("set_warehouse_label");
},
set_from_warehouse: function (frm) {
@@ -499,6 +500,23 @@ frappe.ui.form.on("Material Request", {
if (frm.doc.material_request_type !== "Material Transfer" && frm.doc.set_from_warehouse) {
frm.set_value("set_from_warehouse", "");
}
frm.trigger("set_warehouse_label");
},
set_warehouse_label(frm) {
let warehouse_label =
frm.doc.material_request_type === "Material Transfer" ? "Target Warehouse" : "Warehouse";
if (frm.doc.material_request_type === "Material Issue") {
warehouse_label = "From Warehouse";
}
frm.fields_dict["items"].grid.update_docfield_property("warehouse", "label", __(warehouse_label));
warehouse_label = "Set " + warehouse_label;
frm.set_df_property("set_warehouse", "label", __(warehouse_label));
refresh_field("items");
},
});

View File

@@ -7,7 +7,7 @@ from itertools import groupby
import frappe
from frappe import _, bold
from frappe.model.mapper import get_mapped_doc, map_child_doc
from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
@@ -646,8 +646,8 @@ class PickList(TransactionBase):
product_bundles = self._get_product_bundles()
product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values())
for so_row, item_code in product_bundles.items():
picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code])
for so_row, value in product_bundles.items():
picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[value.item_code])
item_table = "Sales Order Item"
already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True)
frappe.db.set_value(
@@ -743,7 +743,10 @@ class PickList(TransactionBase):
pi_item.serial_no,
(
Case()
.when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty)
.when(
(pi_item.picked_qty > 0) & (pi_item.docstatus == 1),
pi_item.picked_qty - pi_item.delivered_qty,
)
.else_(pi_item.stock_qty)
).as_("picked_qty"),
)
@@ -770,19 +773,23 @@ class PickList(TransactionBase):
if not item.product_bundle_item:
continue
product_bundles[item.sales_order_item] = frappe.db.get_value(
"Sales Order Item",
item.sales_order_item,
"item_code",
product_bundles[item.sales_order_item] = frappe._dict(
{
"item_code": frappe.db.get_value(
"Sales Order Item",
item.sales_order_item,
"item_code",
),
"pick_list_item": item.name,
}
)
return product_bundles
def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]:
# bundle_item_code: Dict[component, qty]
def _get_product_bundle_qty_map(self, bundles) -> dict[str, dict[str, float]]:
product_bundle_qty_map = {}
for bundle_item_code in bundles:
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0})
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
for data in bundles:
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": data.item_code, "disabled": 0})
product_bundle_qty_map[data.item_code] = {item.item_code: item.qty for item in bundle.items}
return product_bundle_qty_map
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
@@ -1388,15 +1395,16 @@ def add_product_bundles_to_delivery_note(
product_bundles = pick_list._get_product_bundles()
product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values())
for so_row, item_code in product_bundles.items():
for so_row, value in product_bundles.items():
sales_order_item = frappe.get_doc("Sales Order Item", so_row)
if sales_order and sales_order_item.parent != sales_order:
continue
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
so_row, product_bundle_qty_map[item_code]
so_row, product_bundle_qty_map[value.item_code]
)
dn_bundle_item.pick_list_item = value.pick_list_item
dn_bundle_item.against_pick_list = pick_list.name
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)

View File

@@ -645,6 +645,46 @@ class TestPickList(FrappeTestCase):
if dn_item.item_code == "_Test Item 2":
self.assertEqual(dn_item.qty, 2)
def test_picklist_reserved_qty_validation(self):
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
warehouse = "_Test Warehouse - _TC"
test_stock_item = "_Test Stock Item"
# Ensure stock item exists
if not frappe.db.exists("Item", test_stock_item):
create_item(
item_code=test_stock_item,
is_stock_item=1,
)
# Add initial stock qty
make_stock_entry(item_code=test_stock_item, to_warehouse=warehouse, qty=15)
# Create SO for 10 qty
sales_order_1 = make_sales_order(item_code=test_stock_item, warehouse=warehouse, qty=10)
# Create and Submit picklist for SO
picklist_1 = create_pick_list(sales_order_1.name)
picklist_1.submit()
# Create DN for 5 qty
dn = create_delivery_note(picklist_1.name)
dn.items[0].qty = 5
dn.save()
dn.submit()
# Verify partly delivered state
picklist_1.reload()
self.assertEqual(picklist_1.status, "Partly Delivered")
# Create another SO (10 qty)
sales_order_2 = make_sales_order(item_code=test_stock_item, warehouse=warehouse, qty=10)
# Expected pick qty = 5
picklist_2 = create_pick_list(sales_order_2.name)
self.assertEqual(picklist_2.locations[0].qty, 5)
def test_picklist_with_multi_uom(self):
warehouse = "_Test Warehouse - _TC"
item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name

View File

@@ -35,15 +35,6 @@ frappe.ui.form.on("Purchase Receipt", {
filters: { company: frm.doc.company },
};
});
frm.set_query("subcontracting_receipt", function () {
return {
filters: {
docstatus: 1,
supplier: frm.doc.supplier,
},
};
});
},
onload: function (frm) {
erpnext.queries.setup_queries(frm, "Warehouse", function () {
@@ -167,24 +158,6 @@ frappe.ui.form.on("Purchase Receipt", {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
subcontracting_receipt: (frm) => {
if (
frm.doc.is_subcontracted === 1 &&
frm.doc.is_old_subcontracting_flow === 0 &&
frm.doc.subcontracting_receipt
) {
frm.set_value("items", null);
erpnext.utils.map_current_doc({
method: "erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_purchase_receipt",
source_name: frm.doc.subcontracting_receipt,
target_doc: frm,
freeze: true,
freeze_message: __("Mapping Purchase Receipt ..."),
});
}
},
toggle_display_account_head: function (frm) {
var enabled = erpnext.is_perpetual_inventory_enabled(frm.doc.company);
frm.fields_dict["items"].grid.set_column_disp(["cost_center"], enabled);
@@ -195,6 +168,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
erpnext.buying.BuyingController
) {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
}

View File

@@ -1248,11 +1248,12 @@
"label": "Named Place"
},
{
"depends_on": "eval: (doc.is_subcontracted && !doc.is_old_subcontracting_flow)",
"depends_on": "subcontracting_receipt",
"fieldname": "subcontracting_receipt",
"fieldtype": "Link",
"label": "Subcontracting Receipt",
"options": "Subcontracting Receipt",
"read_only": 1,
"search_index": 1
},
{
@@ -1300,7 +1301,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2025-11-12 19:53:48.173096",
"modified": "2025-11-27 16:46:30.210628",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -12,7 +12,7 @@ frappe.listview_settings["Purchase Receipt"] = {
],
get_indicator: function (doc) {
if (cint(doc.is_return) == 1 && doc.status == "Return") {
return [__("Return"), "gray", "is_return,=,Yes"];
return [__("Return"), "gray", "is_return,=,1"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
} else if (flt(doc.per_returned, 2) === 100) {

View File

@@ -4448,6 +4448,87 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(srbnb_cost, 1000)
def test_lcv_for_repack_entry(self):
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
create_landed_cost_voucher,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
for item in [
"Potatoes Raw Material Item",
"Fries Finished Goods Item",
]:
create_item(item)
pr = make_purchase_receipt(
item_code="Potatoes Raw Material Item",
warehouse="_Test Warehouse - _TC",
qty=100,
rate=50,
)
wh1 = create_warehouse("WH A1", company=pr.company)
wh2 = create_warehouse("WH A2", company=pr.company)
ste = make_stock_entry(
purpose="Repack",
source="_Test Warehouse - _TC",
item_code="Potatoes Raw Material Item",
qty=100,
company=pr.company,
do_not_save=1,
)
ste.append(
"items",
{
"item_code": "Fries Finished Goods Item",
"qty": 50,
"t_warehouse": wh1,
},
)
ste.append(
"items",
{
"item_code": "Fries Finished Goods Item",
"qty": 50,
"t_warehouse": wh2,
},
)
ste.insert()
ste.submit()
ste.reload()
for row in ste.items:
if row.t_warehouse:
self.assertEqual(row.valuation_rate, 50)
sles = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": ste.doctype, "voucher_no": ste.name, "actual_qty": (">", 0)},
pluck="stock_value_difference",
)
self.assertEqual(sles, [2500.0, 2500.0])
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2000 * -1)
ste.reload()
for row in ste.items:
if row.t_warehouse:
self.assertEqual(row.valuation_rate, 30)
sles = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": ste.doctype, "voucher_no": ste.name, "actual_qty": (">", 0)},
pluck="stock_value_difference",
)
self.assertEqual(sles, [1500.0, 1500.0])
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -9,7 +9,7 @@ frappe.ui.form.on("Quality Inspection", {
},
set_default_company(frm) {
if (!frm.doc.company) {
if (frm.doc.docstatus === 0 && !frm.doc.company) {
frm.set_value("company", frappe.defaults.get_default("company"));
}
},

View File

@@ -1301,7 +1301,7 @@ class SerialandBatchBundle(Document):
sn_table.purchase_document_no,
self.voucher_no if not sn_table.purchase_document_no else self.voucher_no,
)
.where(sn_table.name.isin(serial_nos))
.where((sn_table.name.isin(serial_nos)) & (sn_table.purchase_document_no.isnull()))
).run()
def validate_serial_and_batch_inventory(self):
@@ -2532,6 +2532,9 @@ def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> dict[str, dict]:
child_row = group_by_voucher[key]
if row.serial_no:
child_row["serial_nos"].append(row.serial_no)
child_row["item_row"].qty = len(child_row["serial_nos"]) * (
-1 if row.type_of_transaction == "Outward" else 1
)
if row.batch_no:
child_row["batch_nos"][row.batch_no] += row.qty

View File

@@ -33,6 +33,6 @@ frappe.ui.form.on("Serial No", {
posting_time: frappe.datetime.now_time(),
};
frappe.set_route("query-report", "Serial No Ledger");
}).addClass("btn-primary");
});
},
});

View File

@@ -571,6 +571,14 @@ frappe.ui.form.on("Stock Entry", {
}
},
set_rate_and_fg_qty: function (frm, cdt, cdn) {
frm.events.set_basic_rate(frm, cdt, cdn);
let item = frappe.get_doc(cdt, cdn);
if (item.is_finished_item) {
frm.events.set_fg_completed_qty(frm);
}
},
get_warehouse_details: function (frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (!child.bom_no) {
@@ -833,7 +841,7 @@ frappe.ui.form.on("Stock Entry", {
frm.doc.items.forEach((item) => {
if (item.is_finished_item) {
fg_completed_qty += flt(item.qty);
fg_completed_qty += flt(item.transfer_qty);
}
});
@@ -859,15 +867,11 @@ frappe.ui.form.on("Stock Entry Detail", {
},
qty(frm, cdt, cdn) {
frm.events.set_basic_rate(frm, cdt, cdn);
let item = frappe.get_doc(cdt, cdn);
if (item.is_finished_item) {
frm.events.set_fg_completed_qty(frm);
}
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
},
conversion_factor(frm, cdt, cdn) {
frm.events.set_basic_rate(frm, cdt, cdn);
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
},
s_warehouse(frm, cdt, cdn) {

View File

@@ -243,7 +243,66 @@ class StockEntry(StockController):
self.validate_same_source_target_warehouse_during_material_transfer()
def set_serial_batch_for_disassembly(self):
if self.purpose != "Disassemble":
return
available_materials = get_available_materials(self.work_order, self)
for row in self.items:
warehouse = row.s_warehouse or row.t_warehouse
materials = available_materials.get((row.item_code, warehouse))
if not materials:
continue
batches = defaultdict(float)
serial_nos = []
qty = row.transfer_qty
for batch_no, batch_qty in materials.batch_details.items():
if qty <= 0:
break
batch_qty = abs(batch_qty)
if batch_qty <= qty:
batches[batch_no] = batch_qty
qty -= batch_qty
else:
batches[batch_no] = qty
qty = 0
if materials.serial_nos:
serial_nos = materials.serial_nos[: int(row.transfer_qty)]
if not serial_nos and not batches:
continue
bundle_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"qty": row.transfer_qty,
"type_of_transaction": "Inward" if row.t_warehouse else "Outward",
"company": self.company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle(serial_nos=serial_nos, batch_nos=batches)
row.serial_and_batch_bundle = bundle_doc.name
row.use_serial_batch_fields = 0
row.db_set(
{
"serial_and_batch_bundle": bundle_doc.name,
"use_serial_batch_fields": 0,
}
)
def on_submit(self):
self.set_serial_batch_for_disassembly()
self.validate_closed_subcontracting_order()
self.make_bundle_using_old_serial_batch_fields()
self.update_disassembled_order()
@@ -462,6 +521,7 @@ class StockEntry(StockController):
"project": self.project,
"uom": item.uom,
"s_warehouse": item.s_warehouse,
"is_finished_item": item.is_finished_item,
}
),
for_update=True,
@@ -491,6 +551,9 @@ class StockEntry(StockController):
flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
)
if self.purpose == "Manufacture":
item.set("expense_account", item_details.get("expense_account"))
def validate_fg_completed_qty(self):
item_wise_qty = {}
if self.purpose == "Manufacture" and self.work_order:
@@ -1774,7 +1837,9 @@ class StockEntry(StockController):
if self.purpose == "Material Issue":
ret["expense_account"] = item.get("expense_account") or item_group_defaults.get("expense_account")
if self.purpose == "Manufacture" or not ret.get("expense_account"):
if (self.purpose == "Manufacture" and not args.get("is_finished_item")) or not ret.get(
"expense_account"
):
ret["expense_account"] = frappe.get_cached_value(
"Company", self.company, "stock_adjustment_account"
)
@@ -1850,7 +1915,13 @@ class StockEntry(StockController):
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
items_dict = get_bom_items_as_dict(self.bom_no, self.company, disassemble_qty)
items_dict = get_bom_items_as_dict(
self.bom_no,
self.company,
disassemble_qty,
fetch_exploded=self.use_multi_level_bom,
fetch_qty_in_stock_uom=False,
)
for row in items:
child_row = self.append("items", {})
@@ -1868,7 +1939,7 @@ class StockEntry(StockController):
child_row.qty = disassemble_qty
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
child_row.t_warehouse = row.s_warehouse
child_row.is_finished_item = 0 if row.is_finished_item else 1
def get_items_from_manufacture_entry(self):
@@ -1887,6 +1958,8 @@ class StockEntry(StockController):
"`tabStock Entry Detail`.`is_finished_item`",
"`tabStock Entry Detail`.`batch_no`",
"`tabStock Entry Detail`.`serial_no`",
"`tabStock Entry Detail`.`s_warehouse`",
"`tabStock Entry Detail`.`t_warehouse`",
"`tabStock Entry Detail`.`use_serial_batch_fields`",
],
filters=[
@@ -3253,8 +3326,8 @@ def get_items_from_subcontract_order(source_name, target_doc=None):
return target_doc
def get_available_materials(work_order) -> dict:
data = get_stock_entry_data(work_order)
def get_available_materials(work_order, stock_entry_doc=None) -> dict:
data = get_stock_entry_data(work_order, stock_entry_doc=stock_entry_doc)
available_materials = {}
for row in data:
@@ -3262,6 +3335,9 @@ def get_available_materials(work_order) -> dict:
if row.purpose != "Material Transfer for Manufacture":
key = (row.item_code, row.s_warehouse)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
key = (row.item_code, row.s_warehouse or row.warehouse)
if key not in available_materials:
available_materials.setdefault(
key,
@@ -3272,7 +3348,9 @@ def get_available_materials(work_order) -> dict:
item_data = available_materials[key]
if row.purpose == "Material Transfer for Manufacture":
if row.purpose == "Material Transfer for Manufacture" or (
stock_entry_doc and stock_entry_doc.purpose == "Disassemble" and row.purpose == "Manufacture"
):
item_data.qty += row.qty
if row.batch_no:
item_data.batch_details[row.batch_no] += row.qty
@@ -3312,7 +3390,7 @@ def get_available_materials(work_order) -> dict:
return available_materials
def get_stock_entry_data(work_order):
def get_stock_entry_data(work_order, stock_entry_doc=None):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_voucher_wise_serial_batch_from_bundle,
)
@@ -3344,19 +3422,35 @@ def get_stock_entry_data(work_order):
(stock_entry.name == stock_entry_detail.parent)
& (stock_entry.work_order == work_order)
& (stock_entry.docstatus == 1)
& (stock_entry_detail.s_warehouse.isnotnull())
& (
stock_entry.purpose.isin(
[
"Manufacture",
"Material Consumption for Manufacture",
"Material Transfer for Manufacture",
]
)
)
)
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
).run(as_dict=1)
)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
data = data.where(
stock_entry.purpose.isin(
[
"Disassemble",
"Manufacture",
]
)
)
data = data.where(stock_entry.name != stock_entry_doc.name)
else:
data = data.where(
stock_entry.purpose.isin(
[
"Manufacture",
"Material Consumption for Manufacture",
"Material Transfer for Manufacture",
]
)
)
data = data.where(stock_entry_detail.s_warehouse.isnotnull())
data = data.run(as_dict=1)
if not data:
return []
@@ -3369,6 +3463,9 @@ def get_stock_entry_data(work_order):
if row.purpose != "Material Transfer for Manufacture":
key = (row.item_code, row.s_warehouse, row.name)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
key = (row.item_code, row.s_warehouse or row.warehouse, row.name)
if bundle_data.get(key):
row.update(bundle_data.get(key))

View File

@@ -1267,6 +1267,7 @@ class TestStockEntry(FrappeTestCase):
frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC"),
],
)
frappe.db.set_value("Company", "_Test Company", "stock_adjustment_account", "Stock Adjustment - _TC")
# SE must have atleast one FG
self.assertRaises(FinishedGoodError, se.save)
@@ -1284,6 +1285,9 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(se.value_difference, 0.0)
self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
self.assertEqual(se.items[0].expense_account, "Stock Adjustment - _TC")
self.assertEqual(se.items[1].expense_account, "_Test Account Cost for Goods Sold - _TC")
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle(self):
# Initialize item, batch, warehouse, opening qty

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

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

@@ -647,10 +647,32 @@ class update_entries_after:
if sle.dependant_sle_voucher_detail_no:
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no):
# for repack entries, we need to repost both source and target warehouses
self.update_distinct_item_warehouses_for_repack(sle)
if self.exceptions:
self.raise_exceptions()
def update_distinct_item_warehouses_for_repack(self, sle):
sles = (
frappe.get_all(
"Stock Ledger Entry",
filters={
"voucher_type": "Stock Entry",
"voucher_no": sle.voucher_no,
"actual_qty": (">", 0),
"is_cancelled": 0,
"voucher_detail_no": ("!=", sle.dependant_sle_voucher_detail_no),
},
fields=["*"],
)
or []
)
for dependant_sle in sles:
self.update_distinct_item_warehouses(dependant_sle)
def has_stock_reco_with_serial_batch(self, sle):
if (
sle.voucher_type == "Stock Reconciliation"
@@ -696,6 +718,13 @@ class update_entries_after:
{"item_code": self.item_code, "warehouse": self.args.warehouse}
)
key = (self.item_code, self.args.warehouse)
if key in self.distinct_item_warehouses and self.distinct_item_warehouses[key].get(
"transfer_entry_to_repost"
):
# only repost stock entries
args["filter_voucher_type"] = "Stock Entry"
return list(self.get_sle_after_datetime(args))
def get_dependent_entries_to_fix(self, entries_to_fix, sle):
@@ -729,8 +758,10 @@ class update_entries_after:
if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date):
self.distinct_item_warehouses[key] = val
self.new_items_found = True
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
dependant_sle.voucher_no
elif (
dependant_sle.actual_qty > 0
and dependant_sle.voucher_type == "Stock Entry"
and is_transfer_stock_entry(dependant_sle.voucher_no)
):
if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"):
return
@@ -1156,7 +1187,11 @@ class update_entries_after:
def get_dynamic_incoming_outgoing_rate(self, sle):
# Get updated incoming/outgoing rate from transaction
if sle.recalculate_rate or self.has_landed_cost_based_on_pi(sle):
if (
sle.recalculate_rate
or self.has_landed_cost_based_on_pi(sle)
or (sle.voucher_type == "Stock Entry" and sle.actual_qty > 0 and is_repack_entry(sle.voucher_no))
):
rate = self.get_incoming_outgoing_rate_from_transaction(sle)
if flt(sle.actual_qty) >= 0:
@@ -1813,6 +1848,9 @@ def get_stock_ledger_entries(
if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s"
if previous_sle.get("filter_voucher_type"):
conditions += " and voucher_type = %(filter_voucher_type)s"
if extra_cond:
conditions += f"{extra_cond}"
@@ -1908,8 +1946,7 @@ def get_valuation_rate(
& (table.warehouse == warehouse)
& (table.batch_no == batch_no)
& (table.is_cancelled == 0)
& (table.voucher_no != voucher_no)
& (table.voucher_type != voucher_type)
& ((table.voucher_no != voucher_no) | (table.voucher_type != voucher_type))
)
)
@@ -2454,3 +2491,8 @@ def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj):
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
return incoming_rate
@frappe.request_cache
def is_repack_entry(stock_entry_id):
return frappe.get_cached_value("Stock Entry", stock_entry_id, "purpose") == "Repack"

View File

@@ -187,7 +187,7 @@ class SubcontractingOrder(SubcontractingController):
for item in self.get("items"):
bom = frappe.get_doc("BOM", item.bom)
rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items)
item.rm_cost_per_qty = rm_cost / flt(bom.quantity)
item.rm_cost_per_qty = flt(rm_cost / flt(bom.quantity), item.precision("rm_cost_per_qty"))
def calculate_items_qty_and_amount(self):
total_qty = total = 0

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from collections import defaultdict
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
@@ -17,6 +19,10 @@ from erpnext.stock.get_item_details import get_default_cost_center, get_default_
from erpnext.stock.stock_ledger import get_valuation_rate
class BOMQuantityError(frappe.ValidationError):
pass
class SubcontractingReceipt(SubcontractingController):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -156,6 +162,7 @@ class SubcontractingReceipt(SubcontractingController):
def on_submit(self):
self.validate_closed_subcontracting_order()
self.validate_available_qty_for_consumption()
self.validate_bom_required_qty()
self.update_status_updater_args()
self.update_prevdoc_status()
self.set_subcontracting_order_status(update_bin=False)
@@ -512,12 +519,60 @@ class SubcontractingReceipt(SubcontractingController):
item.available_qty_for_consumption
and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
):
msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)}
must be less than or equal to Available Qty For Consumption
{flt(item.available_qty_for_consumption, precision)}
in Consumed Items Table."""
msg = _(
"""Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption
{3} {4} in Consumed Items Table."""
).format(
item.idx,
flt(item.consumed_qty, precision),
item.stock_uom,
flt(item.available_qty_for_consumption, precision),
item.stock_uom,
)
frappe.throw(_(msg))
frappe.throw(msg)
def validate_bom_required_qty(self):
if (
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
== "Material Transferred for Subcontract"
) and not (frappe.db.get_single_value("Buying Settings", "validate_consumed_qty")):
return
rm_consumed_dict = self.get_rm_wise_consumed_qty()
for row in self.items:
precision = row.precision("qty")
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
required_qty = flt(
bom_item.qty_consumed_per_unit * row.qty * row.conversion_factor, precision
)
consumed_qty = rm_consumed_dict.get(bom_item.rm_item_code, 0)
diff = flt(consumed_qty, precision) - flt(required_qty, precision)
if diff < 0:
msg = _(
"""Additional {0} {1} of item {2} required as per BOM to complete this transaction"""
).format(
frappe.bold(abs(diff)),
frappe.bold(bom_item.stock_uom),
frappe.bold(bom_item.rm_item_code),
)
frappe.throw(
msg,
exc=BOMQuantityError,
)
def get_rm_wise_consumed_qty(self):
rm_dict = defaultdict(float)
for row in self.supplied_items:
rm_dict[row.rm_item_code] += row.consumed_qty
return rm_dict
def update_status_updater_args(self):
if cint(self.is_return):

View File

@@ -38,6 +38,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
make_subcontracting_receipt,
)
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
BOMQuantityError,
)
class TestSubcontractingReceipt(FrappeTestCase):
@@ -174,7 +177,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
def test_subcontracting_over_receipt(self):
"""
Behaviour: Raise multiple SCRs against one SCO that in total
receive more than the required qty in the SCO.
receive more than the required qty in the SCO.
Expected Result: Error Raised for Over Receipt against SCO.
"""
from erpnext.controllers.subcontracting_controller import (
@@ -1785,6 +1788,109 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(scr.items[0].rm_cost_per_qty, 300)
self.assertEqual(scr.items[0].service_cost_per_qty, 100)
def test_bom_required_qty_validation_based_on_bom(self):
set_backflush_based_on("BOM")
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
rm_item1 = make_item(
properties={
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BRQV-.####",
}
).name
make_bom(item=fg_item, raw_materials=[rm_item1], rm_qty=2)
se = make_stock_entry(
item_code=rm_item1,
qty=1,
target="_Test Warehouse 1 - _TC",
rate=300,
)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 1,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 1,
},
]
sco = get_subcontracting_order(service_items=service_items)
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.reload()
self.assertEqual(scr.supplied_items[0].batch_no, batch_no)
self.assertEqual(scr.supplied_items[0].consumed_qty, 1)
self.assertEqual(scr.supplied_items[0].required_qty, 2)
self.assertRaises(BOMQuantityError, scr.submit)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
def test_bom_required_qty_validation_based_on_transfer(self):
from erpnext.controllers.subcontracting_controller import (
make_rm_stock_entry as make_subcontract_transfer_entry,
)
set_backflush_based_on("Material Transferred for Subcontract")
frappe.db.set_single_value("Buying Settings", "validate_consumed_qty", 1)
item_code = "_Test Subcontracted Validation FG Item 1"
rm_item1 = make_item(
properties={
"is_stock_item": 1,
}
).name
make_subcontracted_item(item_code=item_code, raw_materials=[rm_item1])
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 100,
"fg_item": item_code,
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(
service_items=service_items,
include_exploded_items=0,
)
# inward raw material stock
make_stock_entry(target="_Test Warehouse - _TC", item_code=rm_item1, qty=10, basic_rate=100)
rm_items = [
{
"item_code": item_code,
"rm_item_code": sco.supplied_items[0].rm_item_code,
"qty": sco.supplied_items[0].required_qty - 5,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
]
# transfer partial raw materials
ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items))
ste.to_warehouse = "_Test Warehouse 1 - _TC"
ste.save()
ste.submit()
scr = make_subcontracting_receipt(sco.name)
scr.save()
self.assertRaises(BOMQuantityError, scr.submit)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)

View File

@@ -8858,3 +8858,632 @@ CANCELLED,ملغي,
Mobile: ,المحمول: ,
Sending...,إرسال...,
{0} is a mandatory field.,{0} حقل إلزامي.,
Address, العنوان,
Amount, مبلغ,
Is Child Table, جدول تابع,
Name, الاسم,
Rate, سعر السلعة المفردة,
Summary, الملخص,
# In Stock,# في المخزن,
# Req'd Items,# السلع المطلوبة,
% Delivered,% تسليم,
% Finished Item Quantity,% كمية المنتج النهائي,
% Occupied,محجوز,
% Picked,مختار,
'Account' in the Accounting section of Customer {0},'الحساب' في قسم المحاسبة الخاص بالعميل {0},
'Allow Multiple Sales Orders Against a Customer's Purchase Order','السماح بإنشاء طلبات مبيعات متعددة مقابل طلب الشراء الخاص بالعميل',
'Default {0} Account' in Company {1},'حساب {0} الافتراضي' في شركة {1},
"'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI",'الفحص المطلوب قبل التسليم' للعنصر {0} تم تعطيله، لذا لا داعي لإنشاء فحص الجودة,
"'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI",'الفحص المطلوب قبل الشراء' للعنصر {0} قد تم تعطيله، لذا لا داعي لإنشاء فحص الجودة,
'To Package No.' cannot be less than 'From Package No.',لا يمكن أن يكون 'الطرد إلى' أقل من 'الطرد من',
'{0}' account is already used by {1}. Use another account.,{0} الحساب مستخدم بواسطة{1} استخدم حساب آخر.,
'{0}' should be in company currency {1}.,يجب أن يكون '{0}' بعملة الشركة {1}.,
(A) Qty After Transaction,الكَمَيَّة بعد العملية (أ).,
(B) Expected Qty After Transaction,(ب) الكَمَيَّة المتوقعة بعد العملية,
(C) Total Qty in Queue,(ج) إجمالي الكمية في قائمة الانتظار,
(C) Total qty in queue,(ج) إجمالي الكمية في قائمة الانتظار,
(D) Balance Stock Value,(د) قيمة المخزون المتبقي,
(E) Balance Stock Value in Queue,(هـ) قيمة المخزون المتبقي في الطابور,
(F) Change in Stock Value,(و) التغيير في قيمة المخزون,
(G) Sum of Change in Stock Value,(ز) مجموع التغير في قيمة الأسهم,
(H) Change in Stock Value (FIFO Queue),(ح) التغيير في قيمة المخزون (طابور FIFO),
(H) Valuation Rate,(ح) معدل التقييم,
(I) Valuation Rate,(ط) معدل التقييم,
(J) Valuation Rate as per FIFO,(ي) معدل التقييم حسب FIFO,
(K) Valuation = Value (D) ÷ Qty (A),(ك) التقييم = القيمة (د) ÷ الكَمَيَّة (أ),
0-30 Days,0-30 يوماً,
11-50,١١-٥٠,
201-500,٢٠١-٥٠٠,
30-60,٣٠-٦٠,
30-60 Days,30-60 يوماً,
60-90,٦٠-٩٠,
60-90 Days,60-90 يوماً,
90 Above,أكثر من 90,
"<br>
<h4>Note</h4>
<ul>
<li>
You can use <a href=""https://jinja.palletsprojects.com/en/2.11.x/"" target=""_blank"">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.
</li><li>
All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.
</li></ul>
<h4> Examples</h4>
<!-- {% raw %} -->
<ul>
<li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.customer_name }}</code></pre><br></li>
<li><b>Body</b>: <br><br>
<pre><code>Hello {{ customer.customer_name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>
</ul>
<!-- {% endraw %} -->","<br>
<h4>ملاحظة</h4>
<ul>
<li>
يمكنك استخدام <a href=""https://jinja.palletsprojects.com/en/2.11.x/"" target=""_blank"">علامات جينجا</a> في حقول <b>الموضوع</b> و <b>المحتوى</b> للقيم الديناميكية.
</li><li>
جميع الحقول في هذا النوع من الوثائق متاحة ضمن كائن <b>doc</b> وجميع الحقول الخاصة بالعميل الذي سيصل إليه البريد متاحة ضمن كائن <b>customer</b>.
</li></ul>
<h4> أمثلة</h4>
<!-- {% raw %} -->
<ul>
<li><b>الموضوع</b>:<br><br><pre><code>كشف حسابات لـ {{ customer.customer_name }}</code></pre><br></li>
<li><b>المحتوى</b>: <br><br>
<pre><code>مرحبًا {{ customer.customer_name }},<br>يرجى ملاحظة كشف حساباتك من {{ doc.from_date }} إلى {{ doc.to_date }}.</code> </pre></li>
</ul>
<!-- {% endraw %} -->",
"<div class=""columnHeading"">Other Details</div>","<div class=""columnHeading"">تفاصيل أخرى</div>",
"<div class=""text-muted text-center"">No Matching Bank Transactions Found</div>","<div class=""text-muted text-center"">لم يتم العثور على معاملات مصرفية مطابقة</div>",
"<div>
<h3> All dimensions in centimeter only </h3>
</div>","<div>
<h3>كل الأبعاد تقاس بالسنتيمتر فقط</h3>
</div>",
"<h3>About Product Bundle</h3>
<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>
<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>
<h4>Example:</h4>
<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>","<h3>حول حزمة المنتج</h3>
<p>مجموعة مجمعة من <b>عناصر</b> في عنصر <b>آخر</b>. يكون هذا مفيدًا إذا كنت تقوم بتجميع <b>عناصر</b> معينة في حزمة وتحتفظ بمخزون من <b>عناصر معبأة</b> وليس مجموع <b>عنصر</b>.</p>
<p>ستحتوي الحزمة <b>العنصر</b> على <code>هو عنصر مخزون</code> مثل <b>لا</b> و <code>هو عنصر مبيعات</code> مثل <b>نعم</b>.</p>
<h4>مثال:</h4>
<p>إذا كنت تبيع أجهزة الكمبيوتر المحمولة وحقائب الظهر بشكل منفصل ولديك سعر خاص إذا اشترى العميل كليهما، فسيكون الكمبيوتر المحمول + حقيبة الظهر عنصرًا جديدًا في حزمة المنتج.</p>",
"<h3>Currency Exchange Settings Help</h3>
<p>There are 3 variables that could be used within the endpoint, result key and in values of the parameter.</p>
<p>Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.</p>
<p>Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}</p>","<h3>تعليمات إعدادات صرف العملات</h3>
<p>هناك 3 متغيرات يمكن استخدامها داخل نقطة النهاية ومفتاح النتيجة وفي قيم المعلمة.</p>
<p>يتم جلب سعر الصرف بين {from_currency} و {to_currency} في {transaction_date} بواسطة واجهة برمجة التطبيقات.</p>
<p>مثال: إذا كانت نقطة النهاية الخاصة بك هي exchange.com/2021-08-01، فسيتعين عليك إدخال exchange.com/{transaction_date}</p>",
"<h4>Body Text and Closing Text Example</h4>
<div>We have noticed that you have not yet paid invoice {{sales_invoice}} for {{frappe.db.get_value(""Currency"", currency, ""symbol"")}} {{outstanding_amount}}. This is a friendly reminder that the invoice was due on {{due_date}}. Please pay the amount due immediately to avoid any further dunning cost.</div>
<h4>How to get fieldnames</h4>
<p>The fieldnames you can use in your template are the fields in the document. You can find out the fields of any documents via Setup &gt; Customize Form View and selecting the document type (e.g. Sales Invoice)</p>
<h4>Templating</h4>
<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=""strong"" href=""http://jinja.pocoo.org/docs/dev/templates/"">read this documentation.</a></p>","<h4>مثال على نص الجسم والنص الختامي</h4>
<div>لاحظنا أنك لم تدفع الفاتورة {{sales_invoice}} بعد عن {{frappe.db.get_value(""Currency"", currency, ""symbol"")}} {{outstanding_amount}}. هذا تذكير ودي بأن الفاتورة كانت مستحقة في {{due_date}}. يرجى سداد المبلغ المستحق على الفور لتجنب أي تكاليف مطالبة أخرى.</div>
<h4>كيفية الحصول على أسماء الحقول</h4>
<p>أسماء الحقول التي يمكنك استخدامها في القالب الخاص بك هي الحقول الموجودة في المستند. يمكنك معرفة حقول أي مستند عبر الإعداد &gt; تخصيص عرض النموذج واختيار نوع المستند (على سبيل المثال فاتورة المبيعات)</p>
<h4>القوالب</h4>
<p>يتم تجميع القوالب باستخدام لغة قوالب Jinja. لمعرفة المزيد عن Jinja، اقرأ هذه الوثائق <a class=""strong"" href=""http://jinja.pocoo.org/docs/dev/templates/""></a></p>",
"<h4>Contract Template Example</h4>
<pre>Contract for Customer {{ party_name }}
-Valid From : {{ start_date }}
-Valid To : {{ end_date }}
</pre>
<h4>How to get fieldnames</h4>
<p>The field names you can use in your Contract Template are the fields in the Contract for which you are creating the template. You can find out the fields of any documents via Setup &gt; Customize Form View and selecting the document type (e.g. Contract)</p>
<h4>Templating</h4>
<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=""strong"" href=""http://jinja.pocoo.org/docs/dev/templates/"">read this documentation.</a></p>","<h4>مثال على قالب العقد</h4>
<pre>عقد للعميل {{ party_name }}
-صالح من: {{ start_date }}
-صالح إلى: {{ end_date }}
</pre>
<h4>كيفية الحصول على أسماء الحقول</h4>
<p>أسماء الحقول التي يمكنك استخدامها في قالب العقد الخاص بك هي الحقول الموجودة في العقد الذي تقوم بإنشاء القالب له. يمكنك معرفة حقول أي مستند عبر الإعداد &gt; تخصيص عرض النموذج واختيار نوع المستند (على سبيل المثال العقد)</p>
<h4>القوالب</h4>
<p>يتم تجميع القوالب باستخدام لغة قوالب Jinja. لمعرفة المزيد عن Jinja، اقرأ هذه الوثائق <a class=""strong"" href=""http://jinja.pocoo.org/docs/dev/templates/""></a></p>",
"<h4>Standard Terms and Conditions Example</h4>
<pre>Delivery Terms for Order number {{ name }}
-Order Date : {{ transaction_date }}
-Expected Delivery Date : {{ delivery_date }}
</pre>
<h4>How to get fieldnames</h4>
<p>The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup &gt; Customize Form View and selecting the document type (e.g. Sales Invoice)</p>
<h4>Templating</h4>
<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=""strong"" href=""http://jinja.pocoo.org/docs/dev/templates/"">read this documentation.</a></p>","<h4>مثال على الشروط والأحكام القياسية</h4>
<pre>شروط التسليم لرقم الطلب {{ name }}
-تاريخ الطلب: {{ transaction_date }}
-تاريخ التسليم المتوقع: {{ delivery_date }}
</pre>
<h4>كيفية الحصول على أسماء الحقول</h4>
<p>أسماء الحقول التي يمكنك استخدامها في قالب البريد الإلكتروني الخاص بك هي الحقول الموجودة في المستند الذي ترسل منه البريد الإلكتروني. يمكنك معرفة حقول أي مستند عبر الإعداد &gt; تخصيص عرض النموذج واختيار نوع المستند (على سبيل المثال فاتورة المبيعات)</p>
<h4>القوالب</h4>
<p>يتم تجميع القوالب باستخدام لغة قوالب Jinja. لمعرفة المزيد عن Jinja، اقرأ هذه الوثائق <a class=""strong"" href=""http://jinja.pocoo.org/docs/dev/templates/""></a></p>",
"<label class=""control-label"" style=""margin-bottom: 0px;"">Account Number Settings</label>","<label class=""control-label"" style=""margin-bottom: 0px;"">إعدادات رقم الحساب</label>",
"<label class=""control-label"" style=""margin-bottom: 0px;"">Amount In Words</label>","<label class=""control-label"" style=""margin-bottom: 0px;"">المبلغ بالكلمات</label>",
"<label class=""control-label"" style=""margin-bottom: 0px;"">Date Settings</label>","<label class=""control-label"" style=""margin-bottom: 0px;"">إعدادات التاريخ</label>",
<li>Item {0} in row(s) {1} billed more than {2}</li>,<li>المادة {0} في الصف (الصفوف) {1} تمت فوترته أكثر من {2}</li>,
<p>Cannot overbill for the following Items:</p>,<p/>لا يمكن زيادة الفاتورة للعناصر التالية:<p>,
<p>Following {0}s doesn't belong to Company {1} :</p>,<p>:ال{0} التالية لا تنتمى إلي الشركة {1}</p>,
"<p>In your <b>Email Template</b>, you can use the following special variables:
</p>
<ul>
<li>
<code>{{ update_password_link }}</code>: A link where your supplier can set a new password to log into your portal.
</li>
<li>
<code>{{ portal_link }}</code>: A link to this RFQ in your supplier portal.
</li>
<li>
<code>{{ supplier_name }}</code>: The company name of your supplier.
</li>
<li>
<code>{{ contact.salutation }} {{ contact.last_name }}</code>: The contact person of your supplier.
</li><li>
<code>{{ user_fullname }}</code>: Your full name.
</li>
</ul>
<p></p>
<p>Apart from these, you can access all values in this RFQ, like <code>{{ message_for_supplier }}</code> or <code>{{ terms }}</code>.</p>","<p>في قالب البريد الإلكتروني <b></b>، يمكنك استخدام المتغيرات الخاصة التالية:
</p>
<ul>
<li>
<code>{{ update_password_link }}</code>: رابط يمكن لموردك من خلاله تعيين كلمة مرور جديدة لتسجيل الدخول إلى البوابة الخاصة بك.
</li>
<li>
<code>{{ portal_link }}</code>: رابط لهذا الطلب على الأسعار في بوابة المورد الخاص بك.
</li>
<li>
<code>{{ supplier_name }}</code>: اسم شركة المورد الخاص بك.
</li>
<li>
<code>{{ contact.salutation }} {{ contact.last_name }}</code>: جهة الاتصال الخاصة بموردك.
</li><li>
<code>{{ user_fullname }}</code>: اسمك الكامل.
</li>
</ul>
<p></p>
<p>وبصرف النظر عن ذلك، يمكنك الوصول إلى جميع القيم في طلب عرض الأسعار هذا، مثل <code>{{ message_for_supplier }}</code> أو <code>{{ terms }}</code>.</p>",
"<p>Price List Rate has not been set as editable in Selling Settings. In this scenario, setting <strong>Update Price List Based On</strong> to <strong>Price List Rate</strong> will prevent auto-updation of Item Price.</p>Are you sure you want to continue?",<p>معدل قائمة الأسعار غير قابل للتعديل بحسب إعدادات البيع. وفي هذه الحاله فإن<strong>تحديث قائمة الأسعار بناء</strong> على <strong>معدل قائمة الأسعار</strong> سيمنع التحديث التلقائي لسعر المنتج.</p>هل أنت متأكد أنك تريد المتابعة؟,
"<p>To allow over-billing, please set allowance in Accounts Settings.</p>",<p>للسماح بإصدار الفواتير الزائدة، يُرجى ضبط الحد المسموح به في إعدادات الحسابات.</p>,
"<pre><h5>Message Example</h5>
&lt;p&gt; Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.&lt;/p&gt;
&lt;p&gt; Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.&lt;/p&gt;
&lt;p&gt; We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! &lt;/p&gt;
&lt;a href=""{{ payment_url }}""&gt; click here to pay &lt;/a&gt;
</pre>
","<pre><h5>مثال الرسالة</h5>
&lt;p&gt; نشكرك على كونك جزءًا من {{ doc.company }}! نأمل أن تستمتع بالخدمة.&lt;/p&gt;
&lt;p&gt; يُرجى إيجاد كشف حساب الفاتورة الإلكترونية المرفق. المبلغ المستحق هو {{ doc.grand_total }}.&lt;/p&gt;
&lt;p&gt; لا نريدك أن تقضي وقتك في الجري من أجل دفع فاتورتك.<br>بعد كل شيء، الحياة جميلة والوقت الذي بين يديك يجب أن تقضيه للاستمتاع بها!<br>لذا، إليك طرقنا الصغيرة لمساعدتك في الحصول على مزيد من الوقت مدى الحياة! &lt;/p&gt;
&lt;a href=""{{ payment_url }}""&gt; انقر هنا للدفع &lt;/a&gt;
</pre>
",
"<pre><h5>Message Example</h5>
&lt;p&gt;Dear {{ doc.contact_person }},&lt;/p&gt;
&lt;p&gt;Requesting payment for {{ doc.doctype }}, {{ doc.name }} for {{ doc.grand_total }}.&lt;/p&gt;
&lt;a href=""{{ payment_url }}""&gt; click here to pay &lt;/a&gt;
</pre>
","<pre><h5>مثال الرسالة</h5>
&lt;p&gt;عزيزي {{ doc.contact_person }}،&lt;/p&gt;
&lt;p&gt;أطلب الدفع مقابل {{ doc.doctype }}، {{ doc.name }} مقابل {{ doc.grand_total }}.&lt;/p&gt;
&lt;a href=""{{ payment_url }}""&gt; انقر هنا للدفع &lt;/a&gt;
</pre>
",
"<span class=""h4""><b>Masters &amp; Reports</b></span>","<span class=""h4""><b>المعاملات &amp; التقارير</b></span>",
"<span class=""h4""><b>Quick Access</b></span>","<span class=""h4""><b>الوصول السريع</b></span>",
"<span class=""h4""><b>Shortcuts</b></span>","<span class=""h4""><b>الاختصارات</b></span>",
"<span class=""h4""><b>Your Shortcuts
</b></span>","<span class=""h4""><b>اختصاراتك
</b></span>",
<strong>Grand Total:</strong> {0},المجموع الكلي: {0},
<strong>Outstanding Amount:</strong> {0},<strong>المبلغ المستحِقّ:</strong> {0},
"<table class=""table table-bordered table-condensed"">
<thead>
<tr>
<th class=""table-sr"" style=""width: 50%;"">Child Document</th>
<th class=""table-sr"" style=""width: 50%;"">Non Child Document</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<p> To access parent document field use parent.fieldname and to access child table document field use doc.fieldname </p>
</td>
<td>
<p>To access document field use doc.fieldname </p>
</td>
</tr>
<tr>
<td>
<p><b>Example: </b> parent.doctype == ""Stock Entry"" and doc.item_code == ""Test"" </p>
</td>
<td>
<p><b>Example: </b> doc.doctype == ""Stock Entry"" and doc.purpose == ""Manufacture""</p>
</td>
</tr>
</tbody>
</table>
","<table class=""table table-bordered table-condensed"">
<thead>
<tr>
<th class=""table-sr"" style=""width: 50%;"">Child Document</th>
<th class=""table-sr"" style=""width: 50%;"">Non Child Document</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<p> للوصول إلى الوالد في حقل المستند الأصل. اسم ieldname والوصول إلى ملف استخدام حقل ملف جدول الأطفال. الاسم </p>
</td>
<td>
<p>للوصول إلى وثيقة استخدام الحقل الإسم </p>
</td>
</tr>
<tr>
<td>
<p><b>مثال: </b> والد. octype == ""Stock إدخال"" و doc. tem_code == ""اختبار </p>
</td>
<td>
<p><b>مثال: </b> doc. octype == ""Stock إدخال"" و doc. urposed == ""الصناعة""</p>
</td>
</tr>
</tbody>
</table>
",
A - B,أ - ب,
A - C,أ - ج,
A Holiday List can be added to exclude counting these days for the Workstation.,يمكن إضافة قائمة الإجازات لحساب هذه الأيام لمحطة العمل.,
A Packing Slip can only be created for Draft Delivery Note.,لا يمكن إنشاء إيصال التعبئة إلا لمذكرة التسليم المُسَوَّدَة.,
"A Price List is a collection of Item Prices either Selling, Buying, or both",قائمة الأسعار هي مجموعة من أسعار العناصر إما البيع أو الشراء أو كليهما,
A Reconciliation Job {0} is running for the same filters. Cannot reconcile now,يتم تشغيل {0} مهمة التسوية لنفس عوامل التصفية. لا يمكن التصالح الآن,
A Transaction Deletion Document: {0} is triggered for {0},مستند حذف المعاملة: يتم تشغيل {0} ل {0},
A customer must have primary contact email.,يجب أن يكون لدى العميل بريد إلكتروني أساسي للاتصال.,
A driver must be set to submit.,يجب تعيين برنامَج التشغيل للإرسال.,
A template with tax category {0} already exists. Only one template is allowed with each tax category,يوجد بالفعل قالب مع فئة الضريبة {0} . يسمح بقالب واحد فقط مع كل فئة ضريبية,
API Details,تفاصيل واجهة برمجة التطبيقات,
Abbreviation: {0} must appear only once,الاختصار: يجب أن يظهر {0} مرة واحدة فقط,
Acceptance Criteria Formula,معادلة معايير القبول,
Acceptance Criteria Value,قيمة معايير القبول,
Accepted Qty in Stock UOM,الكَمَيَّة المقبولة بوحدة قياس المخزون,
Access Key,مفتاح الوصول,
Access Key is required for Service Provider: {0},مفتاح الوصول مطلوب لموفر الخدمة: {0},
"According to the BOM {0}, the Item '{1}' is missing in the stock entry.",وفقًا لقائمة المواد {0}، فإن العنصر '{1}' مفقود في إدخال المخزون.,
Account Closing Balance,رصيد إغلاق الحساب,
Account Currency (From),عملة الحساب (من),
Account Currency (To),عملة الحساب (إلى),
Account Opening Balance,رصيد فتح الحساب,
Account not Found,تعذر العثور على الحساب,
Account {0} added multiple times,تمت إضافة الحساب {0} عدة مرات,
Account {0} cannot be converted to Group as it is already set as {1} for {2}.,لا يمكن تحويل الحساب {0} إلى مجموعة لأنه تم تعيينه على أنه {1} لـ {2}.,
Account {0} cannot be disabled as it is already set as {1} for {2}.,لا يمكن تعطيل الحساب {0} لأنه تم تعيينه بالفعل على أنه {1} لـ {2}.,
Account {0} doesn't belong to Company {1},الحساب {0} لا ينتمي إلى الشركة {1},
Account {0} is disabled.,تم تعطيل الحساب {0}.,
Accounting Dimension Filter,تحديد الأبعاد المحاسبيه,
Accounting Dimensions Filter,فلتر الأبعاد المحاسبية,
Accounting Entry for {0},القيد المحاسبي لـ {0},
Accounts Closing,إغلاق الحسابات,
Accounts Missing Error,خطأ في الحسابات مفقودة,
Accounts Receivable / Payable Tuning,ضبط الحسابات المدينة/الدائنة,
Accounts Receivable/Payable,حسابات القبض / الدائنة,
Accounts to Merge,الحسابات المراد دمجها,
Action If Quality Inspection Is Rejected,الإجراء إذا تم رفض فحص الجودة,
Action If Same Rate is Not Maintained,الإجراء إذا لم يتم الحفاظ على نفس السعر,
Action if Same Rate is Not Maintained Throughout Internal Transaction,الإجراء في حالة عدم الحفاظ على نفس المعدل طوال المعاملة الداخلية,
Action if Same Rate is Not Maintained Throughout Sales Cycle,الإجراء إذا لم يتم الحفاظ على نفس السعر طوال دورة المبيعات,
Active Status,الحالة النشطة,
Actual Balance Qty,كَمَيَّة الرصيد الفعلي,
Actual Expense,المصروفات الفعلية,
Actual Posting,النشر الفعلي,
Actual Time,الوقت الفعلي,
Add Columns in Transaction Currency,إضافة أعمدة في عملة المعاملات,
Add Corrective Operation Cost in Finished Good Valuation,إضافة تكلفة العملية التصحيحية في التقييم الجيد النهائي,
Add Discount,إضافة خصم,
Add Items in the Purpose Table,إضافة عناصر في جدول الأغراض,
Add Lead to Prospect,تحويل العميل المحتمل إلى عميل مؤهل,
Add Local Holidays,إضافة العطل المحلية,
Add Manually,أضف يدويّاً,
Add Or Deduct,إضافة أو خَصْم,
Add Serial / Batch Bundle,إضافة حزمة تسلسلية / دفعية,
Add Serial / Batch No,إضافة الرَّقْم التسلسلي / رَقْم الدفعة,
Add Serial / Batch No (Rejected Qty),إضافة الرَّقْم تسلسلي / دفعة (الكَمَيَّة المرفوضة),
Add Stock,إضافة مخزونا,
Add Sub Assembly,إضافة مجموعة فرعية,
Add a Note,إضافة ملاحظة,
Add details,إضافة تفاصيل,
Add to Prospect,أضف إلى احتمال,
Added On,تمت إضافته في,
Added Supplier Role to User {0}.,تمت إضافة دور المورد إلى المستخدم {0}.,
Added {1} Role to User {0}.,تمت إضافة دور {1} إلى المستخدم {0}.,
Adding Lead to Prospect...,جاري تحويل العميل المحتمل إلى عميل مؤهل...,
Additional,الإضافية,
Additional Data,بيانات إضافية,
Additional Info,معلومات اضافية,
Address And Contacts,العنوان و جهات الاتصال,
Adjust Asset Value,تعديل قيمة الأصول,
Adjustment Against,التعديل ضد,
Adjustment based on Purchase Invoice rate,التعديل بناءً على سعر وَرقة الحساب,
Advance Account: {0} must be in either customer billing currency: {1} or Company default currency: {2},حساب الدفع المقدم: {0} يجب أن يكون إما بعملة فواتير العميل: {1} أو بعملة الشركة الافتراضية: {2},
Advance Payment,الدفع المسبق,
Advance Payment Date,تاريخ الدفع المسبق,
Advance Tax,الضريبة المسبقة,
Advance Taxes and Charges,الضرائب والرسوم المسبقة,
Advance paid against {0} {1} cannot be greater than Grand Total {2},لا يمكن أن يكون المبلغ المدفوع مقدمًا مقابل {0} {1} أكبر من الإجمالي الكلي {2},
Advance payments allocated against orders will only be fetched,سيتم جلب الدفعات المقدمة المخصصة للطلبات فقط,
Affected Transactions,المعاملات المتأثرة,
Against Customer Order {0},مقابل طلب العميل {0},
Against Pick List,مقابل قائمة الاختيار,
Against Supplier Invoice {0},مقابل فاتورة المورد {0},
Against Voucher No,مقابل القسيمة رَقْم,
Age ({0}),السن ({0}),
Agent Busy Message,رسالة الوكيل مشغول,
Agent Group,مجموعة الوكيل,
Agent Unavailable Message,رسالة عدم توفر الوكيل,
Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item,تجميع مجموعة من العناصر في عنصر آخر. يكون هذا مفيداً إذا كنت تحتفظ بمخزون العناصر المُجمَّعة وليس العنصر المضاف,
Algorithm,الخوارزمية,
All Activities,جميع الأنشطة,
All Activities HTML,جميع الأنشطة HTML,
All Items,جميع العناصر,
All Sales Transactions can be tagged against multiple Sales Persons so that you can set and monitor targets.,يمكن وضع علامة على جميع معاملات المبيعات ضد عدة مندوبي مبيعات حتى تتمكن من تعيين الأهداف ومراقبتها.,
All allocations have been successfully reconciled,تمت تسوية جميع المخصصات بنجاح,
All items have already been received,تم استلام جميع العناصر مسبقاً,
All items in this document already have a linked Quality Inspection.,جميع العناصر في هذا المستند مرتبطة بفحص جودة.,
All the Comments and Emails will be copied from one document to another newly created document(Lead -> Opportunity -> Quotation) throughout the CRM documents.,سيتم نسخ جميع التعليقات ورسائل البريد الإلكتروني من مستند إلى مستند آخر تم إنشاؤه حديثًا (العميل المحتمل -> الفرصة -> عرض الأسعار) في جميع مستندات إدارة علاقات العملاء.,
"All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table.",عميل محتمل,
Allocate Payment Request,تخصيص طلب السداد,
Allocated Entries,الإدخالات المخصصة,
Allocated To:,المخصصة ل:,
Allocations,المخصصات,
Allow Alternative Item must be checked on Item {},يجب تحديد خيار السماح بالعنصر البديل في العنصر {},
Allow Continuous Material Consumption,السماح بالاستهلاك المستمر للمواد,
Allow Excess Material Transfer,السماح بنقل المواد الزائدة,
Allow Implicit Pegged Currency Conversion,السماح بتحويل العملة الضمني,
Allow Item to be Added Multiple Times in a Transaction,السماح بإضافة الصنف أكثر من مرة في نفس المعاملة,
Allow Lead Duplication based on Emails,السماح بتكرار العملاء المحتملين بناءً على رسائل البريد الإلكتروني,
Allow Or Restrict Dimension,السماح أو تقييد البعد,
Allow Partial Payment,السماح بتسديد جزئي,
Allow Partial Reservation,السماح بالحجز الجزئي,
Allow Purchase,السماح بالشراء,
Allow Purchase Order with Zero Quantity,السماح بأمر الشراء بكمية صفر,
Allow Quotation with Zero Quantity,السماح بعرض سعر المورد بكمية صفرية,
Allow Request for Quotation with Zero Quantity,السماح بعرض سعر المورد بكمية صفرية,
Allow Sales,السماح بالمبيعات,
Allow Sales Order Creation For Expired Quotation,السماح بإنشاء أمر بيع لأمر شراء منتهي الصلاحية,
Allow Sales Order with Zero Quantity,السماح بطلب المبيعات بكمية صفر,
Allow Supplier Quotation with Zero Quantity,السماح بعرض سعر المورد بكمية صفرية,
Allow UOM with Conversion Rate Defined in Item,السماح بوحدة القياس مع معدل التحويل المحدد في العنصر,
Allow User to Edit Discount,السماح للمستخدم بتعديل الخصم,
Allow User to Edit Rate,السماح للمستخدم بتحرير السعر,
Allow Zero Rate,السماح بمعدل صفري,
Allow existing Serial No to be Manufactured/Received again,السماح بتصنيع/استلام الرقم التسلسلي الحالي مرة أخرى,
Allow material consumptions without immediately manufacturing finished goods against a Work Order,السماح باستهلاك المواد دون تصنيع السلع النهائية فورًا بموجب أمر العمل,
Allow multi-currency invoices against single party account ,السماح بفواتير متعددة العملات مقابل حساب طرف واحد ,
Allow to Edit Stock UOM Qty for Purchase Documents,السماح بتعديل كمية وحدة قياس المخزون لمستندات الشراء,
Allow to Edit Stock UOM Qty for Sales Documents,السماح بتعديل كمية وحدة قياس المخزون لمستندات المبيعات,
Allow to Make Quality Inspection after Purchase / Delivery,السماح بإجراء فحص الجودة بعد الشراء/التسليم,
Allow transferring raw materials even after the Required Quantity is fulfilled,السماح بنقل المواد الخام حتى بعد استيفاء الكمية المطلوبة,
Allowed Dimension,البُعد المسموح به,
Allowed Doctypes,أنواع المستندات المسموح بها,
Allowed Items,الأصناف المسموح بها,
Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only.,"الأدوار الأساسية المسموح بها هي ""العميل"" و""المورد"". يرجى تحديد أحد هذه الأدوار فقط.",
Allows to keep aside a specific quantity of inventory for a particular order.,يسمح بالاحتفاظ بكمية محددة من المخزون جانباً لطلب معين.,
Allows users to submit Purchase Orders with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.,يسمح للمستخدمين بتقديم طلبات الشراء بكمية صفرية. مفيد عندما تكون الأسعار ثابتة ولكن الكميات ليست كذلك. مثل عقود الأسعار.,
Allows users to submit Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.,يسمح للمستخدمين بتقديم عروض الأسعار بكمية صفرية. مفيد عندما تكون الأسعار ثابتة ولكن الكميات ليست كذلك. مثل عقود الأسعار.,
Allows users to submit Request for Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.,يسمح للمستخدمين بتقديم طلب عروض الأسعار بكمية صفرية. مفيد عندما تكون الأسعار ثابتة ولكن الكميات ليست ثابتة. مثل عقود الأسعار.,
Allows users to submit Sales Orders with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.,يسمح للمستخدمين بتقديم طلبات المبيعات بكمية صفرية. مفيد عندما تكون الأسعار ثابتة ولكن الكميات ليست كذلك. مثل عقود الأسعار.,
Allows users to submit Supplier Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.,يسمح للمستخدمين بتقديم عروض أسعار الموردين بكمية صفرية. مفيد عندما تكون الأسعار ثابتة ولكن الكميات غير ثابتة. مثل عقود الأسعار.,
Already Picked,تم اختياره بالفعل,
Also you can't switch back to FIFO after setting the valuation method to Moving Average for this item.,أيضًا لا يمكنك العودة إلى FIFO بعد ضبط طريقة التقييم على المتوسط المتحرك لهذا العنصر.,
Alternative Items,العناصر البديلة,
"Alternatively, you can download the template and fill your data in.",..,
Amount (AED),المبلغ (بالدرهم الإماراتي),
Amount Difference with Purchase Invoice,فرق المبلغ مع فاتورة الشراء,
Amount Eligible for Commission,المبلغ المؤهل للعمولة,
Amount in Account Currency,المبلغ المدين بعملة الحساب,
Amount in Words,المبلغ بالكلمات,
Amount in party's bank account currency,المبلغ في عملة الحساب المصرفي للطرف,
Amount in transaction currency,المبلغ بعملة المعاملة,
Amounts,المبالغ,
An Item Group is a way to classify items based on types.,مجموعة العناصر هي طريقة لتصنيف العناصر بناءً على الأنواع.,
Analysis Chart,الرسم البياني التحليلي,
Annual Revenue,الإيراد السنوي,
Applicable Dimension,البُعد المطبق,
Applicable On Account,ينطبق على الحساب,
Apply Tax Withholding Amount ,تطبيق مبلغ الاستقطاع الضريبي ,
Appointment Scheduling has been disabled for this site,تم تعطيل جدولة المواعيد لهذا الموقع,
Appointment was created. But no lead was found. Please check the email to confirm,تم تحديد موعد، ولكن لم يتم العثور على أي عميل محتمل. يُرجى مراجعة البريد الإلكتروني للتأكيد,
Approximately match the description/party name against parties,مطابقة الوصف/اسم الطرف تقريبًا مع الأطراف,
Are you sure you want to clear all demo data?,هل أنت متأكد أنك تريد مسح كافة بيانات العرض التوضيحي؟,
Are you sure you want to delete this Item?,هل أنت متأكد أنك تريد حذف هذه المادة؟,
Assembly Items,عناصر التجميع,
Asset Activity,نشاط الأصل,
Asset Depreciation Schedule,جدول استهلاك الأصول,
Asset Depreciation Schedule for Asset {0} and Finance Book {1} is not using shift based depreciation,جدول استهلاك الأصول للأصل {0} ودفتر التمويل {1} لا يستخدم الاستهلاك القائم على الوردية,
Asset ID,معرّف الأصل,
Asset Quantity,كَمَيَّة الأصول,
Asset scrapped,الأصول الملغاة,
Attendance & Leaves,الحضور والانصراف,
Batch No.,رقم دفعة.,
Call Again,معاودة الاتصال,
Call Ended,انتهت المكالمة,
Call Received By,استلمت المكالمة من قبل,
Call Type,نوع المكالمة,
Cannot produce more item for {0},لا يمكن إنتاج المزيد من العناصر لـ {0},
Cannot produce more than {0} items for {1},لا يمكن إنتاج أكثر من {0} عنصرًا لـ {1},
Cannot receive from customer against negative outstanding,لا يمكن الاستلام من العميل مقابل مستحقات سالبة,
Checked On,تم التحقق منه,
Competitors,المنافسون,
Completed On,اكتمل في,
Completed Tasks,المهام المكتملة,
Completed Time,وقت التنفيذ,
Conditional Rule,القاعدة الشرطية,
Conditional Rule Examples,أمثلة على القواعد الشرطية,
Consumed Stock Total Value,القيمة الإجمالية للمخزون المستهلك,
Contact Mobile,رقم الهاتف المحمول,
Contact Person does not belong to the {0},جهة الاتصال لا تنتمي إلى {0},
Contribution Qty,كمية المساهمة,
Conversion rate cannot be 0,لا يمكن أن يكون معدل التحويل 0,
Costing Details,تفاصيل التكلفة,
Create Journal Entries,إنشاء قيد الدفتر اليومي,
Create Link,إنشاء رابط,
Create New Customer,إنشاء عميل جديد,
Create in Draft Status,إنشاء في حالة المسودة,
Create {0} {1} ?,إنشاء {0} {1}؟,
Creating Journal Entries...,جاري إنشاء قيد الدفتر اليومي...,
Creating Purchase Invoices ...,جاري إنشاء فاتورة شراء ...,
Creating Purchase Receipt ...,جاري إنشاء فاتورة شراء ...,
Creating Sales Invoices ...,جاري إنشاء فاتورة المبيعات ...,
Creating User...,جاري إنشاء المستخدم...,
Current Liability,خصوم متداولة,
Customer ,العميل ,
Customer Name: ,اسم العميل: ,
Customer: ,العميل: ,
Date ,التاريخ ,
Dates,التواريخ,
Debit (Transaction),مدين (معاملة),
Debit Amount in Transaction Currency,المبلغ المدين بعملة المعاملة,
Debit-Credit Mismatch,عدم تطابق الرصيد الدائن,
Default Discount Account,حساب الخصم الافتراضي,
Delay (In Days),التأخير (بالأيام),
Delete Dimension,حذف البُعد المحاسبي,
Delivery Manager,مدير التوصيل,
Delivery User,مستخدم التوصيل,
Delivery to,التوصيل إلى,
Demo Company,شركة تجريبية,
Demo data cleared,مُسحت البيانات التجريبية,
Difference Qty,كمية الفرق,
Discount Account,حساب الخصم,
Discount Date,تاريخ الخصم,
Discount Settings,إعدادات الخصم,
DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it.,"لا يُنصح بإضافة أنواع المستندات يدويًا إلى جدول ""أنواع المستندات المستثناة"". يُسمح فقط بإزالة المستندات منه.",
Document Type already used as a dimension,نوع المستند مستخدم بالفعل كبُعد محاسبي,
Email Address (required),عنوان البريد الإلكتروني (إجباري),
Email Digest: {0},ملخص البريد الإلكتروني: {0},
Email or Phone/Mobile of the Contact are mandatory to continue.,البريد الإلكتروني أو الهاتف/الهاتف المحمول لجهة الاتصال إلزامي للمتابعة.,
Email verification failed.,فشل التحقق من البريد الإلكتروني.,
Employee Exit,إنهاء خدمة الموظف,
Enter Manually,أدخل يدويًا,
Enter Serial Nos,أدخل الأرقام التسلسلية,
Enter Visit Details,أدخل تفاصيل الزيارة,
Enter a name for this Holiday List.,أدخل اسمًا لقائمة العطلات هذه.,
Error during caller information update,حدث خطأ أثناء تحديث معلومات المتصل,
Example: Serial No {0} reserved in {1}.,مثال: الرقم التسلسلي {0} محجوز في {1}.,
Failed Entries,الإدخالات الفاشلة,
"Failed to erase demo data, please delete the demo company manually.",فشل مسح البيانات التجريبية، يرجى حذف الشركة التجريبية يدوياً.,
Financial Reports,التقارير المالية,
Full and Final Statement,البيان الكامل والنهائي,
Generated,تم إنشاؤه,
Goals,الأهداف,
Goods,البضائع,
Internal Transfers,التحويلات الداخلية,
Invoice Portion (%),جزء الفاتورة (%),
Items Catalogue,كتالوج العناصر,
No Stock Available Currently,لا يوجد مخزون متوفر حالياً,
No primary email found for customer: {0},لم يتم العثور على بريد إلكتروني أساسي للعميل: {0},
No.,لا.,
Opening Balances,الأرصدة الافتتاحية,
Percentage (%),النسبة المئوية (%),
Please make sure the file you are using has 'Parent Account' column present in the header.,"يرجى التأكد من أن الملف الذي تستخدمه يحتوي على عمود ""الحساب الأب"" في رأس الملف.",
Qty ,الكمية ,
Recent Orders,الطلبات الأخيرة,
Reference No,رقم المرجع,
Reserved Stock,المخزون المحجوز,
Returned Qty ,عاد الكمية ,
Salary Currency,عملة الراتب,
Sales Partner ,شريك المبيعات ,
Select an invoice to load summary data,حدد فاتورة لتحميل ملخص البيانات,
Service Expenses,مصاريف الخدمة,
Service Item,بند الخدمة,
Time in mins.,الوقت بالدقائق.,
Total Items,إجمالي السلع,
Total Time (in Mins),الوقت الإجمالي (بالدقائق),
Transaction Name,اسم المعاملة,
Voucher,سند,
Warning!,تحذير!,
description,وصف,
doctype,dOCTYPE,
fieldname,اسم الحقل,
variance,فرق,
{0} is added multiple times on rows: {1},{0} تمت إضافته عدة مرات في السطور: {1},
{} Open,{} مفتوحة,
Can't render this file because it is too large.

View File

@@ -1,2 +0,0 @@
Accounts,དངུལ་རྩིས།,
Buying,ཉོ་བ།,
1 Accounts དངུལ་རྩིས།
2 Buying ཉོ་བ།

Some files were not shown because too many files have changed in this diff Show More