Merge branch 'develop' into tds-jv

This commit is contained in:
ljain112
2025-10-14 18:40:58 +05:30
144 changed files with 70433 additions and 1144 deletions

View File

@@ -6,7 +6,7 @@ Feature requests are also a great way to take the product forward. New ideas can
When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want.
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com).
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.frappe.io](https://discuss.frappe.io/c/erpnext/6).
### Reply and Closing Policy

View File

@@ -9,7 +9,7 @@ body:
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6)
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
2. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Forum
url: https://discuss.erpnext.com/
url: https://discuss.frappe.io/c/erpnext/6
about: For general QnA, discussions and community help.

View File

@@ -11,7 +11,7 @@ assignees: ''
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
@@ -21,7 +21,7 @@ Please keep in mind that we get many many requests and we can't possibly work on
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
1. Certified ERPNext partners: https://erpnext.com/partners
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
2. Developer community on ERPNext forums: https://discuss.frappe.io/c/framework/5
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
-->

View File

@@ -133,7 +133,7 @@ To setup the repository locally follow the steps mentioned below:
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.

View File

@@ -46,7 +46,8 @@ def validate_service_stop_date(doc):
if (
old_stop_dates
and old_stop_dates.get(item.name)
and item.service_stop_date != old_stop_dates.get(item.name)
and item.service_stop_date
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))

View File

@@ -82,7 +82,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-29 11:37:42.678556",
"modified": "2025-10-13 15:11:58.300836",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Payment Ledger Entry",

View File

@@ -34,3 +34,15 @@ class AdvancePaymentLedgerEntry(Document):
and not frappe.flags.is_reverse_depr_entry
):
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
def on_doctype_update():
frappe.db.add_index(
"Advance Payment Ledger Entry",
["against_voucher_type", "against_voucher_no"],
)
frappe.db.add_index(
"Advance Payment Ledger Entry",
["voucher_type", "voucher_no"],
)

View File

@@ -13,7 +13,7 @@
</div>
{% endif %}
</div>
<h2 class="text-center">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
<h2 class="text-center">{{ _("GENERAL LEDGER") }}</h2>
<div>
{% if filters.party[0] == filters.party_name[0] %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2020-05-22 16:46:18.712954",
"doctype": "DocType",
@@ -69,7 +70,7 @@
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Frequency",
"options": "Weekly\nMonthly\nQuarterly"
"options": "Daily\nWeekly\nBiweekly\nMonthly\nQuarterly"
},
{
"fieldname": "company",
@@ -416,7 +417,7 @@
}
],
"links": [],
"modified": "2025-09-03 14:24:43.608565",
"modified": "2025-10-07 12:19:20.719898",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _
from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document
from frappe.utils import add_days, add_months, format_date, getdate, today
from frappe.utils import add_days, add_months, add_to_date, format_date, getdate, today
from frappe.utils.jinja import validate_template
from frappe.utils.pdf import get_pdf
from frappe.www.printview import get_print_style
@@ -55,7 +55,7 @@ class ProcessStatementOfAccounts(Document):
enable_auto_email: DF.Check
filter_duration: DF.Int
finance_book: DF.Link | None
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
frequency: DF.Literal["Daily", "Weekly", "Biweekly", "Monthly", "Quarterly"]
from_date: DF.Date | None
ignore_cr_dr_notes: DF.Check
ignore_exchange_rate_revaluation_journals: DF.Check
@@ -555,8 +555,9 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
if doc.enable_auto_email and from_scheduler:
new_to_date = getdate(posting_date or today())
if doc.frequency == "Weekly":
new_to_date = add_days(new_to_date, 7)
if doc.frequency in ("Daily", "Weekly", "Biweekly"):
frequency = {"Daily": 1, "Weekly": 7, "Biweekly": 14}
new_to_date = add_days(new_to_date, frequency[doc.frequency])
else:
new_to_date = add_months(new_to_date, 1 if doc.frequency == "Monthly" else 3)
new_from_date = add_months(new_to_date, -1 * doc.filter_duration)

View File

@@ -23,7 +23,7 @@
{% endif %}
</div>
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
<h2 class="text-center" style="margin-top:0">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
<h4 class="text-center">
{{ filters.customer_name }}
</h4>

View File

@@ -81,6 +81,7 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
process_soa = create_process_soa(
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
)
send_emails(process_soa.name, from_scheduler=True)
process_soa.load_from_db()
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))

View File

@@ -2629,6 +2629,38 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
@IntegrationTestCase.change_settings(
"Buying Settings", {"maintain_same_rate": 0, "set_landed_cost_based_on_purchase_invoice_rate": 1}
)
def test_pr_status_rate_adjusted_from_pi(self):
pr = make_purchase_receipt(qty=5, rate=100)
pi = create_purchase_invoice_from_receipt(pr.name)
pi.submit()
pr.reload()
# Inital check
self.assertEqual(pr.status, "Completed")
pi.reload()
pi.cancel()
pi = create_purchase_invoice_from_receipt(pr.name)
pi.items[0].rate = 80
pi.submit()
pr.reload()
# Test 1 : Adjustment amount is negative
self.assertEqual(pr.status, "Completed")
pi.reload()
pi.cancel()
pi = create_purchase_invoice_from_receipt(pr.name)
pi.items[0].rate = 120
pi.submit()
pr.reload()
# Test 2 : Adjustment amount is positive
self.assertEqual(pr.status, "Completed")
def test_opening_invoice_rounding_adjustment_validation(self):
pi = make_purchase_invoice(do_not_save=1)
pi.items[0].rate = 99.98

View File

@@ -912,7 +912,8 @@
"label": "Rejected Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
"print_hide": 1,
"search_index": 1
},
{
"fieldname": "wip_composite_asset",
@@ -984,7 +985,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-03-07 10:21:59.960021",
"modified": "2025-10-14 13:00:54.441511",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -995,4 +996,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -232,7 +232,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, IntegrationTestCase):
company.save()
test_cc = company.cost_center
default_expense_account = company.default_expense_account
default_expense_account = company.service_expense_account
item = make_item(properties={"is_stock_item": 0})

View File

@@ -256,6 +256,18 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
sales_order_btn() {
var me = this;
let filters = {
docstatus: 1,
status: ["not in", ["Closed", "On Hold"]],
per_billed: ["<", 99.99],
company: me.frm.doc.company,
};
if (me.frm.doc.has_subcontracted) {
filters.is_subcontracted = 1;
}
this.$sales_order_btn = this.frm.add_custom_button(
__("Sales Order"),
function () {
@@ -266,12 +278,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
setters: {
customer: me.frm.doc.customer || undefined,
},
get_query_filters: {
docstatus: 1,
status: ["not in", ["Closed", "On Hold"]],
per_billed: ["<", 99.99],
company: me.frm.doc.company,
},
get_query_filters: filters,
allow_child_item_selection: true,
child_fieldname: "items",
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
@@ -798,6 +805,15 @@ frappe.ui.form.on("Sales Invoice", {
},
};
});
frm.set_query("sales_person", "sales_team", function () {
return {
filters: {
is_group: 0,
enabled: 1,
},
};
});
},
onload: function (frm) {
frm.redemption_conversion_factor = null;
@@ -1094,6 +1110,9 @@ frappe.ui.form.on("Sales Invoice", {
if (frm.doc.is_debit_note) {
frm.set_df_property("return_against", "label", __("Adjustment Against"));
}
frm.set_df_property("update_stock", "read_only", frm.doc.has_subcontracted);
frm.toggle_display("update_stock", !frm.doc.has_subcontracted);
},
});

View File

@@ -31,6 +31,7 @@
"amended_from",
"is_created_using_pos",
"pos_closing_entry",
"has_subcontracted",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -2229,6 +2230,14 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
},
{
"default": "0",
"fieldname": "has_subcontracted",
"fieldtype": "Check",
"hidden": 1,
"label": "Has Subcontracted",
"read_only": 1
}
],
"grid_page_length": 50,
@@ -2242,7 +2251,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2025-09-09 14:48:59.472826",
"modified": "2025-10-09 14:48:59.472826",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -8,6 +8,7 @@ from frappe import _, msgprint, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder import Case
from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
from frappe.utils.data import comma_and
@@ -31,7 +32,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
update_voucher_outstanding,
)
@@ -131,6 +131,7 @@ class SalesInvoice(SellingController):
from_date: DF.Date | None
grand_total: DF.Currency
group_same_items: DF.Check
has_subcontracted: DF.Check
ignore_default_payment_terms_template: DF.Check
ignore_pricing_rule: DF.Check
in_words: DF.SmallText | None
@@ -279,10 +280,65 @@ class SalesInvoice(SellingController):
self.indicator_color = "green"
self.indicator_title = _("Paid")
def before_print(self, settings=None):
from frappe.contacts.doctype.address.address import get_address_display_list
super().before_print(settings)
company_details = frappe.get_value(
"Company", self.company, ["company_logo", "website", "phone_no", "email"], as_dict=True
)
required_fields = [
company_details.get("company_logo"),
company_details.get("phone_no"),
company_details.get("email"),
]
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
frappe.msgprint(
_(
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
)
)
return
if not self.company_address and not frappe.has_permission("Sales Invoice", "write", throw=False):
frappe.msgprint(
_(
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
)
)
return
address_display_list = get_address_display_list("Company", self.company)
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
required_fields.append(self.company_address)
required_fields.append(address_line)
if not all(required_fields):
frappe.publish_realtime(
"sales_invoice_before_print",
{
"company_logo": company_details.get("company_logo"),
"website": company_details.get("website"),
"phone_no": company_details.get("phone_no"),
"email": company_details.get("email"),
"address_line": address_line,
"company": self.company,
"company_address": self.company_address,
"name": self.name,
},
user=frappe.session.user,
)
def validate(self):
self.validate_auto_set_posting_time()
super().validate()
self.is_subcontracted()
if not (self.is_pos or self.is_debit_note):
self.so_dn_required()
@@ -355,6 +411,8 @@ class SalesInvoice(SellingController):
self.allow_write_off_only_on_pos()
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.validate_subcontracted_sales_order()
self.validate_scio_self_rm_qty()
def validate_accounts(self):
self.validate_write_off_account()
@@ -521,6 +579,7 @@ class SalesInvoice(SellingController):
self.apply_loyalty_points()
self.process_common_party_accounting()
self.update_billed_qty_in_scio()
def validate_pos_return(self):
if self.is_consolidated:
@@ -651,6 +710,8 @@ class SalesInvoice(SellingController):
):
self.cancel_pos_invoice_credit_note_generated_during_sales_invoice_mode()
self.update_billed_qty_in_scio()
def update_status_updater_args(self):
if not cint(self.update_stock):
return
@@ -785,6 +846,26 @@ class SalesInvoice(SellingController):
timesheet.set_status()
timesheet.db_update_all()
def update_billed_qty_in_scio(self):
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
fieldname = table.returned_qty if self.is_return else table.billed_qty
data = frappe._dict(
{
item.scio_detail: item.stock_qty if self._action == "submit" else -item.stock_qty
for item in self.items
if item.scio_detail
}
)
if data:
case_expr = Case()
for name, qty in data.items():
case_expr = case_expr.when(table.name == name, fieldname + qty)
frappe.qb.update(table).set(fieldname, case_expr).where(
(table.name.isin(list(data.keys()))) & (table.docstatus == 1)
).run()
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
for data in timesheet.time_logs:
if (
@@ -1177,6 +1258,50 @@ class SalesInvoice(SellingController):
if not self.is_pos and self.write_off_account:
self.write_off_account = None
def validate_subcontracted_sales_order(self):
if self.has_subcontracted:
if [item for item in self.items if not item.sales_order and not item.scio_detail]:
frappe.throw(
_(
"All items must be linked to a Sales Order or Subcontracting Inward Order for this Sales Invoice."
)
)
if not all(
frappe.get_all(
"Sales Order",
{"name": ["in", [item.sales_order for item in self.items if item.sales_order]]},
pluck="is_subcontracted",
)
):
frappe.throw(_("All linked Sales Orders must be subcontracted."))
def validate_scio_self_rm_qty(self):
self_rms = [item for item in self.items if item.scio_detail]
if self_rms:
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
query = (
frappe.qb.from_(table)
.select(
table.required_qty, table.consumed_qty, table.billed_qty, table.returned_qty, table.name
)
.where((table.docstatus == 1) & (table.name.isin([item.scio_detail for item in self_rms])))
)
result = query.run(as_dict=True)
data = {item.name: item for item in result}
for item in self_rms:
row = data.get(item.scio_detail)
max_qty = max(row.required_qty, row.consumed_qty) - row.billed_qty - row.returned_qty
if item.stock_qty > max_qty:
frappe.throw(
_("Row #{0}: Stock quantity {1} ({2}) for item {3} cannot exceed {4}").format(
item.idx,
item.stock_qty,
item.stock_uom,
frappe.bold(item.item_code),
frappe.bold(max_qty),
)
)
def validate_write_off_account(self):
if flt(self.write_off_amount) and not self.write_off_account:
self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account")
@@ -2098,6 +2223,23 @@ class SalesInvoice(SellingController):
if update:
self.db_set("status", self.status, update_modified=update_modified)
@frappe.whitelist()
def is_subcontracted(self):
if not self.has_subcontracted:
self.has_subcontracted = bool(
frappe.get_cached_value(
"Sales Order",
{
"name": ["in", [item.sales_order for item in self.items if item.sales_order]],
"is_subcontracted": 1,
},
"name",
)
)
if self.has_subcontracted:
self.update_stock = 0
return self.has_subcontracted
def get_total_in_party_account_currency(doc):
total_fieldname = "grand_total" if doc.disable_rounded_total else "rounded_total"
@@ -2299,7 +2441,7 @@ def make_delivery_note(source_name, target_doc=None):
"cost_center": "cost_center",
},
"postprocess": update_item,
"condition": lambda doc: doc.delivered_by_supplier != 1,
"condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {
@@ -2802,6 +2944,59 @@ def get_loyalty_programs(customer):
return lp_details
@frappe.whitelist()
def save_company_master_details(name, company, details):
from frappe.utils import validate_email_address
if isinstance(details, str):
details = frappe.parse_json(details)
if details.get("email"):
validate_email_address(details.get("email"), throw=True)
company_fields = ["company_logo", "website", "phone_no", "email"]
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
if company_fields_to_update:
frappe.db.set_value("Company", company, company_fields_to_update)
company_address = details.get("company_address")
if details.get("address_line1"):
address_doc = frappe.get_doc(
{
"doctype": "Address",
"address_title": details.get("address_title"),
"address_type": details.get("address_type"),
"address_line1": details.get("address_line1"),
"address_line2": details.get("address_line2"),
"city": details.get("city"),
"state": details.get("state"),
"pincode": details.get("pincode"),
"country": details.get("country"),
"is_your_company_address": 1,
"links": [{"link_doctype": "Company", "link_name": company}],
}
)
address_doc.insert()
company_address = address_doc.name
if company_address:
company_address_display = frappe.db.get_value("Sales Invoice", name, "company_address_display")
if not company_address_display or details.get("address_line1"):
from frappe.query_builder import DocType
SalesInvoice = DocType("Sales Invoice")
(
frappe.qb.update(SalesInvoice)
.set(SalesInvoice.company_address, company_address)
.set(SalesInvoice.company_address_display, get_address_display(company_address))
.where(SalesInvoice.name == name)
).run()
return True
@frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None):
invoice = frappe.get_doc("Sales Invoice", source_name)

View File

@@ -109,6 +109,7 @@
"column_break_vwhb",
"pos_invoice",
"pos_invoice_item",
"scio_detail",
"internal_transfer_section",
"purchase_order",
"column_break_92",
@@ -978,13 +979,20 @@
"options": "POS Invoice",
"print_hide": 1,
"search_index": 1
},
{
"fieldname": "scio_detail",
"fieldtype": "Data",
"hidden": 1,
"label": "SCIO Detail",
"read_only": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-03-07 10:25:30.275246",
"modified": "2025-09-04 11:08:25.583561",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
@@ -995,4 +1003,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -84,6 +84,7 @@ class SalesInvoiceItem(Document):
rate_with_margin: DF.Currency
sales_invoice_item: DF.Data | None
sales_order: DF.Link | None
scio_detail: DF.Data | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.Text | None
service_end_date: DF.Date | None

View File

@@ -0,0 +1,108 @@
<style>
.letter-head {
border-radius: 18px;
padding-right: 12px;
margin-left: 12px;
margin-right: 12px;
}
.letter-head td{
padding: 0px !important;
}
.invoice-header {
width: 100%;
}
.logo-cell {
width: 100px;
text-align: center;
position: relative;
}
.logo-container {
width: 90px;
display: block;
}
.logo-container img {
max-width: 90px;
max-height: 90px;
display: inline-block;
border-radius: 15px;
}
.company-details {
width: 40%;
align-content: center;
}
.company-name {
font-size: 14px;
font-weight: bold;
color: #171717;
margin-bottom: 4px;
}
.invoice-info-cell {
float: right;
vertical-align: top;
}
.invoice-info {
margin-bottom: 2px;
}
.invoice-label {
color: #7C7C7C;
display: inline-block;
width: 60px;
margin-right: 5px;
}
</style>
<table class="invoice-header">
<tbody>
<tr>
<td class="logo-cell" style="vertical-align: middle !important;">
<div class="logo-container">
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %}
{% if company_logo %}
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo">
{% endif %}
</div>
</td>
<td class="company-details">
<div class="company-name">
{{ doc.company }}
</div>
{% if doc.company_address %}
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
{{ company_address.get("address_line1") or "" }}<br>
{% if company_address.get("address_line2") %}{{ company_address.get("address_line2") }}<br>{% endif %}
{{ company_address.get("city") or "" }}, {{ company_address.get("state") or "" }} {{ company_address.get("pincode") or "" }}, {{ company_address.get("country") or "" }}<br>
{% endif %}
</td>
<td class="invoice-info-cell">
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Invoice:") }}</span>
<span>{{ doc.name }}</span>
</div>
{% if company_details.website %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Website:") }}</span>
<span>{{ company_details.website }}</span>
</div>
{% endif %}
{% if company_details.email %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Email:") }}</span>
<span>{{ company_details.email }}</span>
</div>
{% endif %}
{% if company_details.phone_no %}
<div class="invoice-info">
<span class="invoice-label">{{ _("Contact:") }}</span>
<span>{{ company_details.phone_no }}</span>
</div>
{% endif %}
</td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,125 @@
<style>
.print-format-preview {
margin-top: 12px;
}
.letter-head {
border-radius: 18px;
background: #f8f8f8;
padding: 12px;
margin-left: 12px;
margin-right: 12px;
}
.letterhead-container {
width: 100%;
}
.letterhead-container .other-details {
position: absolute;
right: 0;
bottom: 0;
}
.logo-address {
width: 65%;
vertical-align: top;
}
.letter-head .logo {
width: 90px;
display: block;
margin-bottom: 10px;
}
.letter-head .logo img {
border-radius: 15px;
}
.company-name {
color: #171717;
font-weight: bold;
line-height: 23px;
margin-bottom: 5px;
}
.company-address {
color: #171717;
width: 300px;
}
.invoice-title {
font-weight: bold;
}
.invoice-number {
color: #7c7c7c;
}
.contact-title {
color: #7c7c7c;
width: 60px;
display: inline-block;
vertical-align: top;
margin-right: 10px;
}
.contact-value {
color: #171717;
display: inline-block;
}
.letterhead-container td {
padding: 0px !important;
position: relative;
}
</style>
<table class="letterhead-container">
<tbody>
<tr>
<td class="logo-address">
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %} {% if
company_logo %}
<div class="logo">
<img src="{{ frappe.utils.get_url(company_logo) }}" />
</div>
{% endif %}
<div class="company-name">{{ doc.company }}</div>
<div class="company-address">
{% if doc.company_address %}
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
{{ company_address.address_line1 or "" }}<br />
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br /> {% endif %}
{{ company_address.city or "" }}, {{ company_address.state or "" }}
{{ company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
{% endif %}
</div>
</td>
<td style="vertical-align: top">
<div style="height: 90px; margin-bottom: 10px; text-align: right">
<div class="invoice-title">{{ _("Sales Invoice") }}</div>
<div class="invoice-number">{{ doc.name }}</div>
<br />
</div>
<div style="text-align: left; float: right" class="other-details">
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
{% if company_details.website %}
<div>
<span class="contact-title">{{ _("Website:") }}</span
><span class="contact-value">{{ company_details.website }}</span>
</div>
{% endif %}
{% if company_details.email %}
<div>
<span class="contact-title">{{ _("Email:") }}</span
><span class="contact-value">{{ company_details.email }}</span>
</div>
{% endif %}
{% if company_details.phone_no %}
<div>
<span class="contact-title">{{ _("Contact:") }}</span
><span class="contact-value">{{ company_details.phone_no }}</span>
</div>
{% endif %}
</div>
</td>
</tr>
</tbody>
</table>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -354,7 +354,7 @@ def get_asset_details_for_grouped_by_category(filters):
# nosemgrep
return frappe.db.sql(
f"""
SELECT a.name,
SELECT a.name, a.asset_name,
ifnull(sum(case when a.purchase_date < %(from_date)s then
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
a.net_purchase_amount
@@ -583,6 +583,14 @@ def get_columns(filters):
"width": 120,
}
)
columns.append(
{
"label": _("Asset Name"),
"fieldname": "asset_name",
"fieldtype": "Data",
"width": 140,
}
)
columns += [
{

View File

@@ -319,6 +319,7 @@ def get_asset_value_adjustment_map(filters, finance_book):
.select(asset.name.as_("asset"), Sum(gle.debit - gle.credit).as_("adjustment_amount"))
.where(gle.account == aca.fixed_asset_account)
.where(gle.is_cancelled == 0)
.where(gle.is_opening == "No")
.where(company.name == filters.company)
.where(asset.docstatus == 1)
)

View File

@@ -435,7 +435,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
} else {
if (!doc.items.every((item) => item.qty == item.subcontracted_quantity)) {
if (!doc.items.every((item) => item.qty == item.subcontracted_qty)) {
this.frm.add_custom_button(
__("Subcontracting Order"),
() => {

View File

@@ -972,7 +972,7 @@ def make_subcontracting_order(source_name, target_doc=None, save=False, submit=F
return target_doc
else:
frappe.throw(_("This PO has been fully subcontracted."))
frappe.throw(_("This Purchase Order has been fully subcontracted."))
def is_po_fully_subcontracted(po_name):
@@ -980,7 +980,7 @@ def is_po_fully_subcontracted(po_name):
query = (
frappe.qb.from_(table)
.select(table.name)
.where((table.parent == po_name) & (table.qty != table.subcontracted_quantity))
.where((table.parent == po_name) & (table.qty != table.subcontracted_qty))
)
return not query.run(as_dict=True)
@@ -1035,7 +1035,7 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
"material_request_item": "material_request_item",
},
"field_no_map": ["qty", "fg_item_qty", "amount"],
"condition": lambda item: item.qty != item.subcontracted_quantity,
"condition": lambda item: item.qty != item.subcontracted_qty,
},
},
target_doc,

View File

@@ -1095,9 +1095,9 @@ class TestPurchaseOrder(IntegrationTestCase):
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
po.reload()
self.assertEqual(po.items[0].subcontracted_quantity, 5)
self.assertEqual(po.items[1].subcontracted_quantity, 0)
self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
self.assertEqual(po.items[0].subcontracted_qty, 5)
self.assertEqual(po.items[1].subcontracted_qty, 0)
self.assertEqual(po.items[2].subcontracted_qty, 12.5)
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
self.assertEqual(sco.items[0].amount, 2000)
@@ -1133,10 +1133,10 @@ class TestPurchaseOrder(IntegrationTestCase):
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
po.reload()
self.assertEqual(po.items[2].subcontracted_quantity, 25)
self.assertEqual(po.items[2].subcontracted_qty, 25)
sco.cancel()
po.reload()
self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
self.assertEqual(po.items[2].subcontracted_qty, 12.5)
sco = make_subcontracting_order(po.name)
sco.save()

View File

@@ -26,7 +26,7 @@
"quantity_and_rate",
"qty",
"stock_uom",
"subcontracted_quantity",
"subcontracted_qty",
"col_break2",
"uom",
"conversion_factor",
@@ -933,8 +933,9 @@
},
{
"allow_on_submit": 1,
"default": "0",
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
"fieldname": "subcontracted_quantity",
"fieldname": "subcontracted_qty",
"fieldtype": "Float",
"label": "Subcontracted Quantity",
"no_copy": 1,
@@ -947,7 +948,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-13 17:27:43.468602",
"modified": "2025-10-12 10:57:31.552812",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -85,7 +85,7 @@ class PurchaseOrderItem(Document):
stock_qty: DF.Float
stock_uom: DF.Link
stock_uom_rate: DF.Currency
subcontracted_quantity: DF.Float
subcontracted_qty: DF.Float
supplier_part_no: DF.Data | None
supplier_quotation: DF.Link | None
supplier_quotation_item: DF.Link | None

View File

@@ -284,15 +284,15 @@ def get_columns(filters):
def get_message():
return """<span class="indicator">
Valid till : &nbsp;&nbsp;
return f"""<span class="indicator">
{_("Valid Till")}:&nbsp;&nbsp;
</span>
<span class="indicator orange">
Expires in a week or less
{_("Expires in a week or less")}
</span>
&nbsp;&nbsp;
<span class="indicator red">
Expires today / Already Expired
{_("Expires today or already expired")}
</span>"""

View File

@@ -3804,9 +3804,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
any_qty_changed = True
if (
parent.doctype == "Purchase Order"
parent.doctype in ["Sales Order", "Purchase Order"]
and parent.is_subcontracted
and not parent.is_old_subcontracting_flow
and not parent.get("is_old_subcontracting_flow")
):
validate_fg_item_for_subcontracting(d, new_child_flag)
child_item.fg_item_qty = flt(d["fg_item_qty"])
@@ -3891,7 +3891,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.set_qty_as_per_stock_uom()
parent.calculate_taxes_and_totals()
parent.set_total_in_words()
if parent_doctype == "Sales Order":
if parent_doctype == "Sales Order" and not parent.is_subcontracted:
make_packing_list(parent)
parent.set_gross_profit()
frappe.get_cached_doc("Authorization Control").validate_approving_authority(
@@ -3938,6 +3938,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
).format(frappe.bold(parent.name))
)
else: # Sales Order
if parent.is_subcontracted and not parent.can_update_items():
frappe.throw(
_(
"Items cannot be updated as Subcontracting Inward Order is created against the Sales Order {0}."
).format(frappe.bold(parent.name))
)
parent.validate_selling_price()
parent.validate_for_duplicate_items()
parent.validate_warehouse()
@@ -3957,7 +3963,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.validate_uom_is_integer("stock_uom", "stock_qty")
# Cancel and Recreate Stock Reservation Entries.
if parent_doctype == "Sales Order":
if parent_doctype == "Sales Order" and not parent.is_subcontracted:
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
cancel_stock_reservation_entries,
has_reserved_stock,

View File

@@ -854,13 +854,14 @@ def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
if not _bundle_ids:
return frappe._dict({})
return get_serial_batches_based_on_bundle(field, _bundle_ids)
return get_serial_batches_based_on_bundle(doctype, field, _bundle_ids)
def get_serial_batches_based_on_bundle(field, _bundle_ids):
def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
available_dict = frappe._dict({})
batch_serial_nos = frappe.get_all(
"Serial and Batch Bundle",
@@ -872,6 +873,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
"`tabSerial and Batch Bundle`.`voucher_type`",
"`tabSerial and Batch Bundle`.`voucher_no`",
"`tabSerial and Batch Bundle`.`item_code`",
],
filters=[
["Serial and Batch Bundle", "name", "in", _bundle_ids],
@@ -885,6 +887,16 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
if doctype == "Packed Item":
if key is None:
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
if row.voucher_type == "Delivery Note":
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
elif row.voucher_type == "Sales Invoice":
key = frappe.get_cached_value("Sales Invoice Item", key, "sales_invoice_item")
key = (row.item_code, key)
if row.voucher_type in ["Sales Invoice", "Delivery Note"]:
row.qty = -1 * row.qty
@@ -913,6 +925,8 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
if doctype == "Packed Item":
filters = get_filters_for_packed_item(field, reference_ids)
pluck_field = "serial_and_batch_bundle"
if is_rejected:
@@ -926,10 +940,14 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
pluck=pluck_field,
)
if _bundle_ids and doctype == "Packed Item":
return _bundle_ids
if not _bundle_ids:
return {}
del filters["name"]
if "name" in filters:
del filters["name"]
filters[field] = ("in", reference_ids)
@@ -972,10 +990,29 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
return _bundle_ids
def get_filters_for_packed_item(field, reference_ids):
names = []
filters = {"docstatus": 1, "dn_detail": ("in", reference_ids)}
if dns := frappe.get_all("Delivery Note Item", filters=filters, pluck="name"):
names.extend(dns)
filters = {"docstatus": 1, "sales_invoice_item": ("in", reference_ids)}
if sis := frappe.get_all("Sales Invoice Item", filters=filters, pluck="name"):
names.extend(sis)
if names:
reference_ids.extend(names)
return {"docstatus": 1, field: ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
if not qty_field:
qty_field = "stock_qty"
if not hasattr(row, qty_field):
qty_field = "qty"
if not warehouse_field:
warehouse_field = "warehouse"
@@ -1065,6 +1102,9 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
if not qty_field:
qty_field = "stock_qty"
if not hasattr(child_doc, qty_field):
qty_field = "qty"
warehouse = child_doc.get(warehouse_field)
if parent_doc.get("is_internal_customer"):
warehouse = child_doc.get("target_warehouse")

View File

@@ -519,8 +519,15 @@ class SellingController(StockController):
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
continue
item_details = frappe.get_cached_value(
"Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
)
if not self.get("return_against") or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
get_valuation_method(d.item_code) == "Moving Average"
and self.get("is_return")
and not item_details.has_serial_no
and not item_details.has_batch_no
):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
@@ -999,6 +1006,9 @@ def set_default_income_account_for_item(obj):
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if parent.get("is_return") and parent.get("packed_items"):
return
if child.get("use_serial_batch_fields"):
return

View File

@@ -265,6 +265,8 @@ class StatusUpdater(Document):
# if target_ref_field is not specified, the programmer does not want to validate qty / amount
continue
items_to_validate = []
# get unique transactions to update
for d in self.get_all_children():
if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"):
@@ -286,31 +288,63 @@ class StatusUpdater(Document):
)
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
args["name"] = d.get(args["join_field"])
is_from_pp = (
hasattr(d, "production_plan_sub_assembly_item")
and frappe.db.get_value(
"Production Plan Sub Assembly Item",
d.production_plan_sub_assembly_item,
"type_of_manufacturing",
items_to_validate.append(
frappe._dict(
{
"name": d.get(args["join_field"]),
"production_plan_sub_assembly_item": d.get(
"production_plan_sub_assembly_item"
),
"idx": d.idx,
"child_doc": d,
}
)
== "Subcontract"
)
args["item_code"] = "production_item" if is_from_pp else "item_code"
# get all qty where qty > target_field
item = frappe.db.sql(
"""select `{item_code}` as item_code, `{target_ref_field}`,
`{target_field}`, parenttype, parent from `tab{target_dt}`
where `{target_ref_field}` < `{target_field}`
and name=%s and docstatus=1""".format(**args),
args["name"],
as_dict=1,
if items_to_validate:
pp_sub_assembly_items = [
item.production_plan_sub_assembly_item
for item in items_to_validate
if item.production_plan_sub_assembly_item
]
pp_subcontract_items = []
if pp_sub_assembly_items:
pp_subcontract_items = frappe.db.get_all(
"Production Plan Sub Assembly Item",
filters={
"name": ("in", pp_sub_assembly_items),
"type_of_manufacturing": "Subcontract",
},
pluck="name",
)
regular_items = []
pp_items = []
for item in items_to_validate:
if item.production_plan_sub_assembly_item in pp_subcontract_items:
pp_items.append(item.name)
else:
regular_items.append(item.name)
item_details = []
# Query regular items with item_code field
if regular_items:
item_details.extend(self.fetch_items_with_pending_qty(args, "item_code", regular_items))
# Query production plan items with production_item field
if pp_items:
item_details.extend(self.fetch_items_with_pending_qty(args, "production_item", pp_items))
item_lookup = {item.name: item for item in item_details}
for child_item in items_to_validate:
item = item_lookup.get(child_item.name)
if item:
item = item[0]
item["idx"] = d.idx
item["idx"] = child_item.idx
item["target_ref_field"] = args["target_ref_field"].replace("_", " ")
# if not item[args['target_ref_field']]:
@@ -323,6 +357,28 @@ class StatusUpdater(Document):
elif item[args["target_ref_field"]]:
self.check_overflow_with_allowance(item, args)
def fetch_items_with_pending_qty(self, args, item_field, items):
doctype = frappe.qb.DocType(args["target_dt"])
item_field = doctype[item_field]
target_ref_field = doctype[args["target_ref_field"]]
target_field = doctype[args["target_field"]]
return (
frappe.qb.from_(doctype)
.select(
doctype.name,
item_field.as_("item_code"),
target_ref_field,
target_field,
doctype.parenttype,
doctype.parent,
)
.where(target_ref_field < target_field)
.where(doctype.name.isin(items))
.where(doctype.docstatus == 1)
.run(as_dict=True)
)
def check_overflow_with_allowance(self, item, args):
"""
Checks if there is overflow condering a relaxation allowance

View File

@@ -95,9 +95,11 @@ class StockController(AccountsController):
"Stock Reconciliation",
]:
for item in self.get("items"):
if (item.get("valuation_rate") == 0 or item.get("incoming_rate") == 0) and item.get(
"allow_zero_valuation_rate"
) == 0:
if (
(item.get("valuation_rate") == 0 or item.get("incoming_rate") == 0)
and item.get("allow_zero_valuation_rate") == 0
and frappe.get_cached_value("Item", item.item_code, "is_stock_item")
):
frappe.toast(
_(
"Row #{0}: Item {1} has zero rate but 'Allow Zero Valuation Rate' is not enabled."
@@ -360,10 +362,20 @@ class StockController(AccountsController):
return
child_doctype = self.doctype + " Item"
if table_name == "packed_items":
field = "parent_detail_docname"
child_doctype = "Packed Item"
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
for row in self.get(table_name):
if data := available_dict.get(row.get(field)):
value = row.get(field)
if table_name == "packed_items" and row.get("parent_detail_docname"):
value = self.get_value_for_packed_item(row)
if not value:
continue
if data := available_dict.get(value):
data = filter_serial_batches(self, data, row)
bundle = make_serial_batch_bundle_for_return(data, row, self)
row.db_set(
@@ -379,6 +391,14 @@ class StockController(AccountsController):
"incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
)
def get_value_for_packed_item(self, row):
parent_items = self.get("items", {"name": row.parent_detail_docname})
if parent_items:
ref = parent_items[0].get("dn_detail")
return (row.item_code, ref)
return None
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
field = {
"Sales Invoice": "sales_invoice_item",
@@ -413,6 +433,12 @@ class StockController(AccountsController):
):
reference_ids.append(row.get(field))
if table_name == "packed_items" and row.get("parent_detail_docname"):
parent_rows = self.get("items", {"name": row.parent_detail_docname}) or []
for d in parent_rows:
if d.get(field) and not d.get(bundle_field):
reference_ids.append(d.get(field))
return field, reference_ids
@frappe.request_cache
@@ -520,10 +546,14 @@ class StockController(AccountsController):
break
elif row.batch_no:
batches = frappe.get_all(
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": row.serial_and_batch_bundle}
batches = sorted(
frappe.get_all(
"Serial and Batch Entry",
filters={"parent": row.serial_and_batch_bundle, "batch_no": ("is", "set")},
pluck="batch_no",
distinct=True,
)
)
batches = sorted([d.batch_no for d in batches])
if batches != [row.batch_no]:
throw_error = True

View File

@@ -35,6 +35,14 @@ class SubcontractingController(StockController):
"order_supplied_items_field": "Purchase Order Item Supplied",
}
)
elif self.doctype == "Subcontracting Inward Order":
self.subcontract_data = frappe._dict(
{
"order_doctype": "Subcontracting Inward Order",
"order_field": "subcontracting_inward_order",
"rm_detail_field": "scio_detail",
}
)
else:
self.subcontract_data = frappe._dict(
{
@@ -47,14 +55,22 @@ class SubcontractingController(StockController):
)
def before_validate(self):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
if self.doctype in [
"Subcontracting Order",
"Subcontracting Inward Order",
"Subcontracting Receipt",
]:
self.remove_empty_rows()
self.set_items_conversion_factor()
def validate(self):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt", "Subcontracting Inward Order"]:
self.validate_items()
self.create_raw_materials_supplied()
self.create_raw_materials_supplied_or_received(
raw_material_table="supplied_items"
if self.doctype != "Subcontracting Inward Order"
else "received_items"
)
self.set_valuation_rate_for_rm()
else:
super().validate()
@@ -109,7 +125,7 @@ class SubcontractingController(StockController):
)
def remove_empty_rows(self):
for key in ["service_items", "items", "supplied_items"]:
for key in ["service_items", "items", "supplied_items", "received_items"]:
if self.get(key):
idx = 1
for item in self.get(key)[:]:
@@ -133,33 +149,47 @@ class SubcontractingController(StockController):
if not is_stock_item:
frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name))
if (
self.doctype == "Subcontracting Inward Order"
and item.delivery_warehouse == self.customer_warehouse
):
frappe.throw(
_(
"Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}."
).format(item.idx, frappe.bold(item.item_name))
)
if not item.get("is_scrap_item"):
if not is_sub_contracted_item:
frappe.throw(
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
)
if (
self.doctype == "Subcontracting Order" and not item.subcontracting_conversion_factor
): # this condition will only be true if user has recently updated from develop branch
service_item_qty = frappe.get_value(
"Subcontracting Order Service Item",
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
fieldname=["qty"],
if self.doctype != "Subcontracting Receipt" and item.qty > flt(
get_pending_subcontracted_quantity(
self.doctype,
self.purchase_order if self.doctype == "Subcontracting Order" else self.sales_order,
).get(
item.purchase_order_item
if self.doctype == "Subcontracting Order"
else item.sales_order_item
)
item.subcontracting_conversion_factor = service_item_qty / item.qty
if self.doctype not in "Subcontracting Receipt" and item.qty > flt(
get_pending_subcontracted_quantity(self.purchase_order).get(item.purchase_order_item)
/ item.subcontracting_conversion_factor,
frappe.get_precision("Purchase Order Item", "qty"),
frappe.get_precision(
"Purchase Order Item"
if self.doctype == "Subcontracting Order"
else "Sales Order Item",
"qty",
),
):
frappe.throw(
_(
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
).format(item.idx, item.item_name)
)
item.amount = item.qty * item.rate
if self.doctype != "Subcontracting Inward Order":
item.amount = item.qty * item.rate
if item.bom:
is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
@@ -198,10 +228,16 @@ class SubcontractingController(StockController):
self.__changed_name = []
self.__reference_name = []
if self.doctype in ["Purchase Order", "Subcontracting Order"] or self.is_new():
if (
self.doctype in ["Purchase Order", "Subcontracting Order", "Subcontracting Inward Order"]
or self.is_new()
):
self.set(self.raw_material_table, [])
return
if not self.get(self.raw_material_table):
return
item_dict = self.__get_data_before_save()
if not item_dict:
return True
@@ -217,8 +253,13 @@ class SubcontractingController(StockController):
self.__changed_name.extend(item_dict.keys())
def __get_backflush_based_on(self):
self.backflush_based_on = frappe.db.get_single_value(
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
self.backflush_based_on = (
frappe.db.get_single_value(
"Buying Settings",
"backflush_raw_materials_of_subcontract_based_on",
)
if self.subcontract_data.order_doctype == "Subcontracting Order"
else "Material Transferred for Subcontract"
)
def initialized_fields(self):
@@ -230,7 +271,7 @@ class SubcontractingController(StockController):
def __get_subcontract_orders(self):
self.subcontract_orders = []
if self.doctype in ["Purchase Order", "Subcontracting Order"]:
if self.doctype in ["Purchase Order", "Subcontracting Order", "Subcontracting Inward Order"]:
return
self.subcontract_orders = [
@@ -540,8 +581,13 @@ class SubcontractingController(StockController):
return frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
def __update_reserve_warehouse(self, row, item):
if self.doctype == self.subcontract_data.order_doctype:
if (
self.doctype == self.subcontract_data.order_doctype
and self.doctype != "Subcontracting Inward Order"
):
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
elif frappe.get_cached_value("Item", row.rm_item_code, "is_customer_provided_item"):
row.warehouse = self.customer_warehouse
def __set_alternative_item(self, bom_item):
if self.alternative_item_details.get(bom_item.rm_item_code):
@@ -616,7 +662,7 @@ class SubcontractingController(StockController):
return serial_nos
def __add_supplied_item(self, item_row, bom_item, qty):
def __add_supplied_or_received_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
if rm_obj.get("qty"):
@@ -629,7 +675,8 @@ class SubcontractingController(StockController):
if self.doctype == self.subcontract_data.order_doctype:
rm_obj.required_qty = flt(qty, rm_obj.precision("required_qty"))
rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount"))
if self.doctype != "Subcontracting Inward Order":
rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount"))
else:
rm_obj.consumed_qty = flt(qty, rm_obj.precision("consumed_qty"))
rm_obj.required_qty = flt(bom_item.required_qty or qty, rm_obj.precision("required_qty"))
@@ -640,7 +687,8 @@ class SubcontractingController(StockController):
if use_serial_batch_fields:
rm_obj.use_serial_batch_fields = 1
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if not self.flags.get("reset_raw_materials"):
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if self.doctype == "Subcontracting Receipt":
if not use_serial_batch_fields:
@@ -657,6 +705,9 @@ class SubcontractingController(StockController):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
from erpnext.stock.get_item_details import get_filtered_serial_nos
if self.is_return:
return
for row in self.supplied_items:
item_details = frappe.get_cached_value(
"Item", row.rm_item_code, ["has_batch_no", "has_serial_no"], as_dict=1
@@ -834,14 +885,14 @@ class SubcontractingController(StockController):
return qty
def __set_supplied_items(self):
def __set_supplied_or_received_items(self):
self.bom_items = {}
has_supplied_items = True if self.get(self.raw_material_table) else False
has_items = True if self.get(self.raw_material_table) else False
for row in self.items:
if self.doctype != self.subcontract_data.order_doctype and (
(self.__changed_name and row.name not in self.__changed_name)
or (has_supplied_items and not self.__changed_name)
or (has_items and not self.__changed_name)
):
continue
@@ -855,7 +906,7 @@ class SubcontractingController(StockController):
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)
self.__add_supplied_item(row, bom_item, qty)
self.__add_supplied_or_received_item(row, bom_item, qty)
elif self.backflush_based_on != "BOM":
for key, transfer_item in self.available_materials.items():
@@ -865,7 +916,7 @@ class SubcontractingController(StockController):
) and transfer_item.qty > 0:
qty = flt(self.__get_qty_based_on_material_transfer(row, transfer_item))
transfer_item.qty -= qty
self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
self.__add_supplied_or_received_item(row, transfer_item.get("item_details"), qty)
if self.qty_to_be_received:
self.qty_to_be_received[
@@ -933,13 +984,13 @@ class SubcontractingController(StockController):
):
return row
def __prepare_supplied_items(self):
def __prepare_supplied_or_received_items(self):
self.initialized_fields()
self.__get_subcontract_orders()
self.__get_pending_qty_to_receive()
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
self.__set_supplied_or_received_items()
self.__modify_serial_and_batch_bundle()
self.__set_rate_for_serial_and_batch_bundle()
@@ -966,7 +1017,7 @@ class SubcontractingController(StockController):
msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}"
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
def __validate_supplied_items(self):
def __validate_supplied_or_received_items(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
return
@@ -984,10 +1035,10 @@ class SubcontractingController(StockController):
self.raw_material_table = raw_material_table
self.__identify_change_in_item_table()
self.__prepare_supplied_items()
self.__validate_supplied_items()
self.__prepare_supplied_or_received_items()
self.__validate_supplied_or_received_items()
def create_raw_materials_supplied(self, raw_material_table="supplied_items"):
def create_raw_materials_supplied_or_received(self, raw_material_table="supplied_items"):
self.set_materials_for_subcontracted_items(raw_material_table)
if self.doctype in ["Subcontracting Receipt", "Purchase Receipt", "Purchase Invoice"]:
@@ -1240,14 +1291,16 @@ def get_item_details(items):
return item_details
def get_pending_subcontracted_quantity(po_name):
table = frappe.qb.DocType("Purchase Order Item")
def get_pending_subcontracted_quantity(doctype, name):
table = frappe.qb.DocType(
"Purchase Order Item" if doctype == "Subcontracting Order" else "Sales Order Item"
)
query = (
frappe.qb.from_(table)
.select(table.name, table.qty, table.subcontracted_quantity)
.where(table.parent == po_name)
.select(table.name, table.stock_qty, table.subcontracted_qty)
.where(table.parent == name)
)
return {item.name: item.qty - item.subcontracted_quantity for item in query.run(as_dict=True)}
return {item.name: item.stock_qty - item.subcontracted_qty for item in query.run(as_dict=True)}
@frappe.whitelist()

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@ class TestSubcontractingController(IntegrationTestCase):
def test_create_raw_materials_supplied(self):
sco = get_subcontracting_order()
sco.supplied_items = None
sco.create_raw_materials_supplied()
sco.create_raw_materials_supplied_or_received()
self.assertIsNotNone(sco.supplied_items)
def test_sco_with_bom(self):

View File

@@ -51,6 +51,8 @@ doctype_list_js = {
],
}
page_js = {"print": "public/js/print.js"}
extend_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
override_whitelisted_methods = {"frappe.www.contact.send_message": "erpnext.templates.utils.send_message"}
@@ -600,6 +602,7 @@ user_privacy_documents = [
},
]
# ERPNext doctypes for Global Search
global_search_doctypes = {
"Default": [

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-10-05 09:35+0000\n"
"PO-Revision-Date: 2025-10-06 10:13\n"
"PO-Revision-Date: 2025-10-09 11:15\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Persian\n"
"MIME-Version: 1.0\n"
@@ -4127,7 +4127,7 @@ msgstr "همه این آیتم‌ها قبلاً صورتحساب/بازگردا
#: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:85
#: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:92
msgid "Allocate"
msgstr "اختصاص دهید"
msgstr ""
#. Label of the allocate_advances_automatically (Check) field in DocType 'POS
#. Invoice'
@@ -5628,7 +5628,7 @@ msgstr "اسلات رزرو قرار"
#: erpnext/crm/doctype/appointment/appointment.py:95
msgid "Appointment Confirmation"
msgstr "تایید قرار ملاقات"
msgstr "تأیید قرار ملاقات"
#: erpnext/www/book_appointment/index.js:237
msgid "Appointment Created Successfully"
@@ -5661,7 +5661,7 @@ msgstr "ملاقات با"
#: erpnext/crm/doctype/appointment/appointment.py:101
msgid "Appointment was created. But no lead was found. Please check the email to confirm"
msgstr "قرار ملاقات ایجاد شد. اما سرنخی پیدا نشد. لطفا برای تایید ایمیل را بررسی کنید"
msgstr "قرار ملاقات ایجاد شد. اما سرنخی پیدا نشد. لطفا برای تأیید ایمیل را بررسی کنید"
#. Label of the approving_role (Link) field in DocType 'Authorization Rule'
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
@@ -5675,7 +5675,7 @@ msgstr "نقش تأیید نمی‌تواند با نقشی که قانون بر
#. Label of the approving_user (Link) field in DocType 'Authorization Rule'
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
msgid "Approving User (above authorized value)"
msgstr "تایید کاربر (بالاتر از مقدار مجاز)"
msgstr "تأیید کاربر (بالاتر از مقدار مجاز)"
#: erpnext/setup/doctype/authorization_rule/authorization_rule.py:77
msgid "Approving User cannot be same as user the rule is Applicable To"
@@ -9434,7 +9434,7 @@ msgstr "برنامه های کمپین"
#: erpnext/setup/doctype/authorization_control/authorization_control.py:60
msgid "Can be approved by {0}"
msgstr "قابل تایید توسط {0}"
msgstr "قابل تأیید توسط {0}"
#: erpnext/manufacturing/doctype/work_order/work_order.py:2172
msgid "Can not close Work Order. Since {0} Job Cards are in Work In Progress state."
@@ -10655,7 +10655,7 @@ msgstr "هنگامی که فایل فشرده به سند پیوست شد، رو
#: erpnext/templates/emails/confirm_appointment.html:3
msgid "Click on the link below to verify your email and confirm the appointment"
msgstr "برای تایید ایمیل خود و تایید قرار ملاقات روی لینک زیر کلیک کنید"
msgstr "برای تأیید ایمیل خود و تأیید قرار ملاقات روی لینک زیر کلیک کنید"
#: erpnext/selling/page/point_of_sale/pos_item_cart.js:485
msgid "Click to add email / phone"
@@ -12000,7 +12000,7 @@ msgstr ""
#. Label of the final_confirmation_date (Date) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json
msgid "Confirmation Date"
msgstr "تاریخ تایید"
msgstr "تاریخ تأیید"
#. Label of the connections_tab (Tab Break) field in DocType 'Purchase Invoice'
#. Label of the connections_tab (Tab Break) field in DocType 'Sales Invoice'
@@ -23223,7 +23223,7 @@ msgstr "درصد سود ناخالص"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:171
msgid "Gross Profit Ratio"
msgstr ""
msgstr "نسبت سود ناخالص"
#. Label of the gross_purchase_amount (Currency) field in DocType 'Asset
#. Depreciation Schedule'
@@ -24763,7 +24763,7 @@ msgstr "در تولید"
#: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.json
#: erpnext/telephony/doctype/call_log/call_log.json
msgid "In Progress"
msgstr "در حال پیش رفت"
msgstr "در حال انجام"
#: erpnext/stock/report/available_serial_no/available_serial_no.py:112
#: erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py:82
@@ -26030,7 +26030,7 @@ msgstr "تنظیمات موجودی"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:214
msgid "Inventory Turnover Ratio"
msgstr ""
msgstr "نسبت گردش موجودی"
#: erpnext/setup/setup_wizard/data/industry_type.txt:29
msgid "Investment Banking"
@@ -32558,7 +32558,7 @@ msgstr "سود خالص"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:172
msgid "Net Profit Ratio"
msgstr ""
msgstr "نسبت سود خالص"
#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:180
msgid "Net Profit/Loss"
@@ -34796,12 +34796,12 @@ msgstr "سفارش توسط"
#. Order'
#: erpnext/buying/doctype/purchase_order/purchase_order.json
msgid "Order Confirmation Date"
msgstr "تاریخ تایید سفارش"
msgstr "تاریخ تأیید سفارش"
#. Label of the order_confirmation_no (Data) field in DocType 'Purchase Order'
#: erpnext/buying/doctype/purchase_order/purchase_order.json
msgid "Order Confirmation No"
msgstr "شماره تایید سفارش"
msgstr "شماره تأیید سفارش"
#: erpnext/crm/report/campaign_efficiency/campaign_efficiency.py:23
#: erpnext/crm/report/lead_owner_efficiency/lead_owner_efficiency.py:29
@@ -38140,7 +38140,7 @@ msgstr "لطفاً شناسه مشتری Plaid و مقادیر مخفی خود
#: erpnext/crm/doctype/appointment/appointment.py:98
#: erpnext/www/book_appointment/index.js:235
msgid "Please check your email to confirm the appointment"
msgstr "لطفا ایمیل خود را برای تایید قرار ملاقات بررسی کنید"
msgstr "لطفا ایمیل خود را برای تأیید قرار ملاقات بررسی کنید"
#: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:374
msgid "Please click on 'Generate Schedule'"
@@ -38263,7 +38263,7 @@ msgstr "لطفاً حساب را برای تغییر مبلغ وارد کنید"
#: erpnext/setup/doctype/authorization_rule/authorization_rule.py:75
msgid "Please enter Approving Role or Approving User"
msgstr "لطفاً نقش تأیید یا تأیید کاربر را وارد کنید"
msgstr "لطفاً نقش تأیید یا کاربر تأیید را وارد کنید"
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:945
msgid "Please enter Cost Center"
@@ -59037,11 +59037,11 @@ msgstr "تا زمان"
#. Option for the 'Status' (Select) field in DocType 'Appointment'
#: erpnext/crm/doctype/appointment/appointment.json
msgid "Unverified"
msgstr "تایید نشده"
msgstr "تأیید نشده"
#: erpnext/erpnext_integrations/utils.py:22
msgid "Unverified Webhook Data"
msgstr "داده های وب هوک تایید نشده"
msgstr "داده های وب هوک تأیید نشده"
#: erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js:17
msgid "Up"
@@ -60173,7 +60173,7 @@ msgstr "تأیید انجام نشد لطفاً پیوند را بررسی کن
#. Label of the verified_by (Data) field in DocType 'Quality Inspection'
#: erpnext/stock/doctype/quality_inspection/quality_inspection.json
msgid "Verified By"
msgstr "تایید شده توسط"
msgstr "تأیید شده توسط"
#: erpnext/templates/emails/confirm_appointment.html:6
#: erpnext/www/book_appointment/verify/index.html:4
@@ -62198,7 +62198,7 @@ msgstr "نام شما (الزامی)"
#: erpnext/www/book_appointment/verify/index.html:11
msgid "Your email has been verified and your appointment has been scheduled"
msgstr "ایمیل شما تایید شده و قرار ملاقات شما تعیین شده است"
msgstr "ایمیل شما تأیید شده و قرار ملاقات شما تعیین شده است"
#: erpnext/patches/v11_0/add_default_dispatch_notification_template.py:22
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:320

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-10-05 09:35+0000\n"
"PO-Revision-Date: 2025-10-06 10:14\n"
"PO-Revision-Date: 2025-10-08 10:44\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: French\n"
"MIME-Version: 1.0\n"
@@ -41601,7 +41601,7 @@ msgstr "Analyse des bons de commande"
#: erpnext/buying/report/procurement_tracker/procurement_tracker.py:76
msgid "Purchase Order Date"
msgstr "Date du de la Commande d'Achat"
msgstr "Date de la commande d'achat"
#. Label of the po_detail (Data) field in DocType 'Purchase Invoice Item'
#. Label of the purchase_order_item (Data) field in DocType 'Sales Invoice

File diff suppressed because it is too large Load Diff

63249
erpnext/locale/my.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-10-05 09:35+0000\n"
"PO-Revision-Date: 2025-10-06 10:13\n"
"PO-Revision-Date: 2025-10-12 11:20\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Polish\n"
"MIME-Version: 1.0\n"
@@ -2525,7 +2525,7 @@ msgstr "Zajęcia"
#: erpnext/projects/doctype/task/task_dashboard.py:8
#: erpnext/support/doctype/issue/issue_dashboard.py:5
msgid "Activity"
msgstr ""
msgstr "Aktywność"
#. Name of a DocType
#. Label of a Link in the Projects Workspace
@@ -7829,7 +7829,7 @@ msgstr ""
#: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
#: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
msgid "Barcode"
msgstr ""
msgstr "Kod kreskowy"
#. Label of the barcode_type (Select) field in DocType 'Item Barcode'
#: erpnext/stock/doctype/item_barcode/item_barcode.json
@@ -9425,7 +9425,7 @@ msgstr ""
#: erpnext/setup/setup_wizard/data/marketing_source.txt:9
#: erpnext/stock/doctype/delivery_note/delivery_note.json
msgid "Campaign"
msgstr ""
msgstr "Kampania"
#. Name of a report
#. Label of a Link in the CRM Workspace
@@ -10162,7 +10162,7 @@ msgstr ""
#. Label of the category (Link) field in DocType 'UOM Conversion Factor'
#: erpnext/setup/doctype/uom_conversion_factor/uom_conversion_factor.json
msgid "Category"
msgstr ""
msgstr "Kategoria"
#. Label of the category_details_section (Section Break) field in DocType 'Tax
#. Withholding Category'
@@ -10932,7 +10932,7 @@ msgstr "Poziom kolekcji"
#: erpnext/setup/doctype/holiday_list/holiday_list.json
#: erpnext/setup/doctype/vehicle/vehicle.json
msgid "Color"
msgstr ""
msgstr "Kolor"
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:263
msgid "Colour"
@@ -14219,7 +14219,7 @@ msgstr ""
#: erpnext/stock/doctype/price_list/price_list.json
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
msgid "Currency"
msgstr ""
msgstr "Waluta"
#. Label of a Link in the Accounting Workspace
#. Name of a DocType
@@ -14461,7 +14461,7 @@ msgstr "Opieka"
#. Exchange Settings'
#: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
msgid "Custom"
msgstr ""
msgstr "Niestandardowy"
#. Label of the custom_remarks (Check) field in DocType 'Payment Entry'
#: erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -18261,7 +18261,7 @@ msgstr ""
#: erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json
#: erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json
msgid "DocType"
msgstr ""
msgstr "DocType"
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:77
msgid "DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."
@@ -19057,7 +19057,7 @@ msgstr ""
#: erpnext/selling/page/point_of_sale/pos_item_cart.js:939
#: erpnext/setup/doctype/company/company.json
msgid "Email"
msgstr ""
msgstr "E-mail"
#. Label of a Card Break in the Settings Workspace
#: erpnext/setup/workspace/settings/settings.json
@@ -19071,7 +19071,7 @@ msgstr ""
#: erpnext/setup/workspace/settings/settings.json
#: erpnext/support/doctype/issue/issue.json
msgid "Email Account"
msgstr ""
msgstr "Konto e-mail"
#. Label of the email_id (Data) field in DocType 'Warehouse'
#. Label of a field in the addresses Web Form
@@ -20838,7 +20838,7 @@ msgstr "Ujmujący..."
#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:106
#: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:155
msgid "Field"
msgstr ""
msgstr "Pole"
#. Label of the field_mapping_section (Section Break) field in DocType
#. 'Inventory Dimension'
@@ -20869,12 +20869,12 @@ msgstr ""
#: erpnext/accounts/doctype/pos_search_fields/pos_search_fields.json
#: erpnext/portal/doctype/website_filter_field/website_filter_field.json
msgid "Fieldname"
msgstr ""
msgstr "Nazwa pola"
#. Label of the fields (Table) field in DocType 'Item Variant Settings'
#: erpnext/stock/doctype/item_variant_settings/item_variant_settings.json
msgid "Fields"
msgstr ""
msgstr "Pola"
#. Description of the 'Do not update variants on save' (Check) field in DocType
#. 'Item Variant Settings'
@@ -20894,7 +20894,7 @@ msgstr "Plik to zmiany nazwy"
#: erpnext/edi/doctype/code_list/code_list_import.js:65
msgid "Filter"
msgstr ""
msgstr "Filtr"
#: erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js:16
#: erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js:16
@@ -23294,7 +23294,7 @@ msgstr ""
#: erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py:17
msgid "Group"
msgstr ""
msgstr "Grupa"
#: erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js:30
#: erpnext/accounts/report/gross_profit/gross_profit.js:36
@@ -23620,7 +23620,7 @@ msgstr ""
#: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
#: erpnext/templates/pages/help.html:3 erpnext/templates/pages/help.html:5
msgid "Help"
msgstr ""
msgstr "Pomoc"
#. Label of the help_section (Tab Break) field in DocType 'Pricing Rule'
#: erpnext/accounts/doctype/pricing_rule/pricing_rule.json
@@ -23806,7 +23806,7 @@ msgstr ""
#. Name of a Workspace
#: erpnext/setup/workspace/home/home.json
msgid "Home"
msgstr ""
msgstr "Strona główna"
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
@@ -28854,7 +28854,7 @@ msgstr ""
#: erpnext/accounts/doctype/pos_field/pos_field.json
#: erpnext/stock/doctype/item_website_specification/item_website_specification.json
msgid "Label"
msgstr ""
msgstr "Etykieta"
#. Label of the taxes (Table) field in DocType 'Landed Cost Voucher'
#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json
@@ -28928,7 +28928,7 @@ msgstr "Krajobraz"
#. Label of the language (Link) field in DocType 'Dunning Letter Text'
#: erpnext/accounts/doctype/dunning_letter_text/dunning_letter_text.json
msgid "Language"
msgstr ""
msgstr "Język"
#. Option for the 'Fulfilment Status' (Select) field in DocType 'Contract'
#: erpnext/crm/doctype/contract/contract.json
@@ -30348,7 +30348,7 @@ msgstr ""
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:254
#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:101
msgid "Mandatory"
msgstr ""
msgstr "Obowiązkowy"
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:99
msgid "Mandatory Accounting Dimension"
@@ -31393,7 +31393,7 @@ msgstr ""
#: erpnext/projects/doctype/project/project.json
#: erpnext/selling/doctype/sms_center/sms_center.json
msgid "Message"
msgstr ""
msgstr "Wiadomość"
#. Label of the message_examples (HTML) field in DocType 'Payment Gateway
#. Account'
@@ -32310,7 +32310,7 @@ msgstr ""
#: erpnext/selling/doctype/quotation/quotation.js:274
#: erpnext/setup/doctype/employee_group/employee_group.json
msgid "Name"
msgstr ""
msgstr "Nazwa"
#. Label of the name_and_employee_id (Section Break) field in DocType 'Sales
#. Person'
@@ -32946,7 +32946,7 @@ msgstr ""
#: erpnext/www/book_appointment/index.html:34
msgid "Next"
msgstr ""
msgstr "Następny"
#. Label of the next_depreciation_date (Date) field in DocType 'Asset'
#: erpnext/assets/doctype/asset/asset.json
@@ -33010,7 +33010,7 @@ msgstr "Kolejny e-mali zostanie wysłany w dniu:"
#: erpnext/stock/doctype/shipment/shipment.json
#: erpnext/stock/doctype/stock_entry/stock_entry.json
msgid "No"
msgstr ""
msgstr "Nie"
#: erpnext/setup/doctype/company/test_company.py:99
msgid "No Account matched these filters: {}"
@@ -33694,7 +33694,7 @@ msgstr ""
#. Label of a Link in the Settings Workspace
#: erpnext/setup/workspace/settings/settings.json
msgid "Notification Settings"
msgstr ""
msgstr "Ustawienia powiadomień"
#: erpnext/stock/doctype/delivery_trip/delivery_trip.js:45
msgid "Notify Customers via Email"
@@ -34530,7 +34530,7 @@ msgstr ""
#: erpnext/manufacturing/workspace/manufacturing/manufacturing.json
#: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:328
msgid "Operation"
msgstr ""
msgstr "Operacja"
#. Label of the production_section (Section Break) field in DocType 'Job Card'
#: erpnext/manufacturing/doctype/job_card/job_card.json
@@ -34803,7 +34803,7 @@ msgstr ""
#. Label of the options (Text) field in DocType 'POS Field'
#: erpnext/accounts/doctype/pos_field/pos_field.json
msgid "Options"
msgstr ""
msgstr "Opcje"
#. Option for the 'Color' (Select) field in DocType 'Supplier Scorecard Scoring
#. Standing'
@@ -36537,7 +36537,7 @@ msgstr ""
#: erpnext/buying/doctype/supplier_scorecard_scoring_variable/supplier_scorecard_scoring_variable.json
#: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.json
msgid "Path"
msgstr ""
msgstr "Ścieżka"
#. Option for the 'Status' (Select) field in DocType 'Job Card Operation'
#: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:96
@@ -37687,7 +37687,7 @@ msgstr "Farmaceutyczne"
#: erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json
#: erpnext/utilities/web_form/addresses/addresses.json
msgid "Phone"
msgstr ""
msgstr "Telefon"
#. Label of the phone_ext (Data) field in DocType 'Lead'
#. Label of the phone_ext (Data) field in DocType 'Opportunity'
@@ -40258,7 +40258,7 @@ msgstr ""
#. Label of a Card Break in the Settings Workspace
#: erpnext/setup/workspace/settings/settings.json
msgid "Printing"
msgstr ""
msgstr "Drukowanie"
#. Label of the printing_details (Section Break) field in DocType 'Material
#. Request'
@@ -48391,7 +48391,7 @@ msgstr ""
#: erpnext/public/js/call_popup/call_popup.js:169
#: erpnext/selling/page/point_of_sale/pos_payment.js:62
msgid "Save"
msgstr ""
msgstr "Zapisz"
#. Option for the 'Action on New Invoice' (Select) field in DocType 'POS
#. Profile'
@@ -50398,7 +50398,7 @@ msgstr ""
#: erpnext/stock/workspace/stock/stock.json
#: erpnext/support/workspace/support/support.json
msgid "Settings"
msgstr ""
msgstr "Ustawienia"
#. Description of a DocType
#: erpnext/crm/doctype/crm_settings/crm_settings.json
@@ -51259,7 +51259,7 @@ msgstr ""
#: erpnext/stock/doctype/delivery_note/delivery_note.json
#: erpnext/templates/form_grid/stock_entry_grid.html:29
msgid "Source"
msgstr ""
msgstr "Źródło"
#. Label of the source_doctype (Link) field in DocType 'Support Search Source'
#: erpnext/support/doctype/support_search_source/support_search_source.json
@@ -52007,7 +52007,7 @@ msgstr "Stan / prowincja"
#: erpnext/templates/pages/task_info.html:69
#: erpnext/templates/pages/timelog_info.html:40
msgid "Status"
msgstr ""
msgstr "Status"
#. Label of the status_details (Section Break) field in DocType 'Service Level
#. Agreement'
@@ -53161,7 +53161,7 @@ msgstr ""
#: erpnext/support/web_form/issues/issues.json
#: erpnext/templates/pages/task_info.html:44
msgid "Subject"
msgstr ""
msgstr "Temat"
#: erpnext/accounts/doctype/payment_order/payment_order.js:139
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.js:323
@@ -54289,7 +54289,7 @@ msgstr ""
#: erpnext/utilities/doctype/video/video.json
#: erpnext/utilities/doctype/video_settings/video_settings.json
msgid "System Manager"
msgstr ""
msgstr "Menedżer systemu"
#. Label of a Link in the Settings Workspace
#. Label of a shortcut in the Settings Workspace
@@ -54384,7 +54384,7 @@ msgstr ""
#: erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json
#: erpnext/templates/form_grid/stock_entry_grid.html:36
msgid "Target"
msgstr ""
msgstr "Cel"
#. Label of the target_amount (Float) field in DocType 'Target Detail'
#: erpnext/setup/doctype/target_detail/target_detail.json
@@ -55205,7 +55205,7 @@ msgstr ""
#: erpnext/quality_management/doctype/quality_feedback/quality_feedback.json
#: erpnext/stock/doctype/item/item_list.js:20
msgid "Template"
msgstr ""
msgstr "Szablon"
#: erpnext/manufacturing/doctype/bom/bom.js:376
msgid "Template Item"
@@ -56557,7 +56557,7 @@ msgstr "Szczeliny czasowe"
#: erpnext/utilities/doctype/video/video.json
#: erpnext/utilities/report/youtube_interactions/youtube_interactions.py:23
msgid "Title"
msgstr ""
msgstr "Tytuł"
#. Label of the email_to (Data) field in DocType 'Payment Request'
#. Label of the to_uom (Link) field in DocType 'UOM Conversion Factor'
@@ -57082,7 +57082,7 @@ msgstr "Zbyt wiele kolumn. Wyeksportować raport i wydrukować go za pomocą ark
#: erpnext/setup/doctype/email_digest/email_digest.json
#: erpnext/stock/workspace/stock/stock.json
msgid "Tools"
msgstr ""
msgstr "Narzędzia"
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
@@ -58491,7 +58491,7 @@ msgstr ""
#: erpnext/stock/report/bom_search/bom_search.py:43
#: erpnext/telephony/doctype/call_log/call_log.json
msgid "Type"
msgstr ""
msgstr "Typ"
#. Label of the type_of_call (Link) field in DocType 'Call Log'
#: erpnext/telephony/doctype/call_log/call_log.json
@@ -58875,7 +58875,7 @@ msgstr ""
#: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js:10
#: erpnext/projects/doctype/project/project_dashboard.html:7
msgid "Unknown"
msgstr ""
msgstr "Nieznany"
#: erpnext/public/js/call_popup/call_popup.js:110
msgid "Unknown Caller"
@@ -59541,7 +59541,7 @@ msgstr "Używane do Planu Produkcji"
#: erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json
#: erpnext/utilities/doctype/portal_user/portal_user.json
msgid "User"
msgstr ""
msgstr "Użytkownik"
#. Label of the section_break_5 (Section Break) field in DocType 'POS Closing
#. Entry'
@@ -59618,7 +59618,7 @@ msgstr ""
#: erpnext/projects/doctype/project/project.json
#: erpnext/projects/doctype/project_update/project_update.json
msgid "Users"
msgstr ""
msgstr "Użytkownicy"
#. Description of the 'Track Semi Finished Goods' (Check) field in DocType
#. 'BOM'
@@ -59963,7 +59963,7 @@ msgstr ""
#: erpnext/stock/report/stock_analytics/stock_analytics.js:26
#: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py:101
msgid "Value"
msgstr ""
msgstr "Wartość"
#: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:58
msgid "Value (G - D)"
@@ -60214,7 +60214,7 @@ msgstr ""
#. Label of the version (Data) field in DocType 'Code List'
#: erpnext/edi/doctype/code_list/code_list.json
msgid "Version"
msgstr ""
msgstr "Wersja"
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
@@ -61156,7 +61156,7 @@ msgstr ""
#: erpnext/setup/workspace/settings/settings.json
#: erpnext/stock/doctype/manufacturer/manufacturer.json
msgid "Website"
msgstr ""
msgstr "Strona internetowa"
#. Name of a DocType
#: erpnext/portal/doctype/website_attribute/website_attribute.json
@@ -61197,7 +61197,7 @@ msgstr ""
#. Label of a Link in the Settings Workspace
#: erpnext/setup/workspace/settings/settings.json
msgid "Website Settings"
msgstr "Ustawienia witryny"
msgstr ""
#. Label of the sb_web_spec (Section Break) field in DocType 'BOM'
#: erpnext/manufacturing/doctype/bom/bom.json
@@ -62018,7 +62018,7 @@ msgstr ""
#: erpnext/stock/doctype/shipment/shipment.json
#: erpnext/stock/doctype/stock_entry/stock_entry.json
msgid "Yes"
msgstr ""
msgstr "Tak"
#: erpnext/edi/doctype/code_list/code_list_import.js:29
msgid "You are importing data for the code list:"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-10-05 09:35+0000\n"
"PO-Revision-Date: 2025-10-06 10:13\n"
"PO-Revision-Date: 2025-10-08 10:44\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Serbian (Cyrillic)\n"
"MIME-Version: 1.0\n"
@@ -14063,7 +14063,7 @@ msgstr "Ограничење потраживања премашено за ку
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:213
msgid "Creditor Turnover Ratio"
msgstr ""
msgstr "Коефицијент обрта добављача"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:87
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:118
@@ -14460,7 +14460,7 @@ msgstr "Тренутна количина"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:152
msgid "Current Ratio"
msgstr ""
msgstr "Рацио опште ликвидности"
#. Label of the current_serial_and_batch_bundle (Link) field in DocType 'Stock
#. Reconciliation Item'
@@ -15752,11 +15752,11 @@ msgstr "Дугује-Потражује нису у равнотежи"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:170
msgid "Debt Equity Ratio"
msgstr ""
msgstr "Рацио структуре капитала"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:212
msgid "Debtor Turnover Ratio"
msgstr ""
msgstr "Коефицијент обрта купаца"
#: erpnext/accounts/party.py:617
msgid "Debtor/Creditor"
@@ -21451,7 +21451,7 @@ msgstr "Регистар основних средстава"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:211
msgid "Fixed Asset Turnover Ratio"
msgstr ""
msgstr "Коефицијент обрта основних средстава"
#: erpnext/manufacturing/doctype/bom/bom.py:668
msgid "Fixed Asset item {0} cannot be used in BOMs."
@@ -23320,7 +23320,7 @@ msgstr "Проценат бруто профита"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:171
msgid "Gross Profit Ratio"
msgstr ""
msgstr "Стопа пословног добитка"
#. Label of the gross_purchase_amount (Currency) field in DocType 'Asset
#. Depreciation Schedule'
@@ -26130,7 +26130,7 @@ msgstr "Поставке инвентара"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:214
msgid "Inventory Turnover Ratio"
msgstr ""
msgstr "Коефицијент обрта залиха"
#: erpnext/setup/setup_wizard/data/industry_type.txt:29
msgid "Investment Banking"
@@ -29648,7 +29648,7 @@ msgstr "Линк за дневник позива"
#: erpnext/accounts/report/financial_ratios/financial_ratios.js:55
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:150
msgid "Liquidity Ratios"
msgstr ""
msgstr "Показатељи ликвидности"
#. Description of the 'Items' (Section Break) field in DocType 'Product Bundle'
#: erpnext/selling/doctype/product_bundle/product_bundle.json
@@ -32659,7 +32659,7 @@ msgstr "Нето профит"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:172
msgid "Net Profit Ratio"
msgstr ""
msgstr "Стопа нето добитка"
#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:180
msgid "Net Profit/Loss"
@@ -42969,7 +42969,7 @@ msgstr "Брзи налог књижења"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:152
msgid "Quick Ratio"
msgstr ""
msgstr "Рацио редуциране ликвидности"
#. Name of a DocType
#. Label of a Link in the Stock Workspace
@@ -45949,11 +45949,11 @@ msgstr "Повраћај компоненти"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:173
msgid "Return on Asset Ratio"
msgstr ""
msgstr "Стопа приноса на имовину"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:174
msgid "Return on Equity Ratio"
msgstr ""
msgstr "Стопа приноса на капитал"
#. Option for the 'Tracking Status' (Select) field in DocType 'Shipment'
#: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:138
@@ -51297,7 +51297,7 @@ msgstr "Продато од"
#: erpnext/accounts/report/financial_ratios/financial_ratios.js:55
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:168
msgid "Solvency Ratios"
msgstr ""
msgstr "Показатељи солвентности"
#: erpnext/www/book_appointment/index.js:248
msgid "Something went wrong please try again"
@@ -58524,7 +58524,7 @@ msgstr "Уторак"
#: erpnext/accounts/report/financial_ratios/financial_ratios.js:55
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:198
msgid "Turnover Ratios"
msgstr ""
msgstr "Показатељи обртаја"
#. Option for the 'Frequency To Collect Progress' (Select) field in DocType
#. 'Project'

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-10-05 09:35+0000\n"
"PO-Revision-Date: 2025-10-06 10:13\n"
"PO-Revision-Date: 2025-10-08 10:45\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Serbian (Latin)\n"
"MIME-Version: 1.0\n"
@@ -14063,7 +14063,7 @@ msgstr "Ograničenje potraživanja premašeno za kupca {0}"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:213
msgid "Creditor Turnover Ratio"
msgstr ""
msgstr "Koeficijent obrta dobavljača"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:87
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:118
@@ -14460,7 +14460,7 @@ msgstr "Trenutna količina"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:152
msgid "Current Ratio"
msgstr ""
msgstr "Racio opšte likvidnosti"
#. Label of the current_serial_and_batch_bundle (Link) field in DocType 'Stock
#. Reconciliation Item'
@@ -15752,11 +15752,11 @@ msgstr "Duguje-Potražuje nisu u ravnoteži"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:170
msgid "Debt Equity Ratio"
msgstr ""
msgstr "Racio strukture kapitala"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:212
msgid "Debtor Turnover Ratio"
msgstr ""
msgstr "Koeficijent obrta kupaca"
#: erpnext/accounts/party.py:617
msgid "Debtor/Creditor"
@@ -21451,7 +21451,7 @@ msgstr "Registar osnovnih sredstava"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:211
msgid "Fixed Asset Turnover Ratio"
msgstr ""
msgstr "Koeficijent obrta osnovnih sredstava"
#: erpnext/manufacturing/doctype/bom/bom.py:668
msgid "Fixed Asset item {0} cannot be used in BOMs."
@@ -23320,7 +23320,7 @@ msgstr "Procenat bruto profita"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:171
msgid "Gross Profit Ratio"
msgstr ""
msgstr "Stopa poslovnog dobitka"
#. Label of the gross_purchase_amount (Currency) field in DocType 'Asset
#. Depreciation Schedule'
@@ -26130,7 +26130,7 @@ msgstr "Postavke inventara"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:214
msgid "Inventory Turnover Ratio"
msgstr ""
msgstr "Koeficijent obrta zaliha"
#: erpnext/setup/setup_wizard/data/industry_type.txt:29
msgid "Investment Banking"
@@ -29648,7 +29648,7 @@ msgstr "Link za dnevnik poziva"
#: erpnext/accounts/report/financial_ratios/financial_ratios.js:55
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:150
msgid "Liquidity Ratios"
msgstr ""
msgstr "Pokazatelji likvidnosti"
#. Description of the 'Items' (Section Break) field in DocType 'Product Bundle'
#: erpnext/selling/doctype/product_bundle/product_bundle.json
@@ -32659,7 +32659,7 @@ msgstr "Neto profit"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:172
msgid "Net Profit Ratio"
msgstr ""
msgstr "Stopa neto dobitka"
#: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:180
msgid "Net Profit/Loss"
@@ -42969,7 +42969,7 @@ msgstr "Brzi nalog knjiženja"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:152
msgid "Quick Ratio"
msgstr ""
msgstr "Racio reducirane likvidnosti"
#. Name of a DocType
#. Label of a Link in the Stock Workspace
@@ -45949,11 +45949,11 @@ msgstr "Povraćaj komponenti"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:173
msgid "Return on Asset Ratio"
msgstr ""
msgstr "Stopa prinosa na imovinu"
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:174
msgid "Return on Equity Ratio"
msgstr ""
msgstr "Stopa prinosa na kapital"
#. Option for the 'Tracking Status' (Select) field in DocType 'Shipment'
#: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:138
@@ -51297,7 +51297,7 @@ msgstr "Prodato od"
#: erpnext/accounts/report/financial_ratios/financial_ratios.js:55
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:168
msgid "Solvency Ratios"
msgstr ""
msgstr "Pokazatelji solventnosti"
#: erpnext/www/book_appointment/index.js:248
msgid "Something went wrong please try again"
@@ -58524,7 +58524,7 @@ msgstr "Utorak"
#: erpnext/accounts/report/financial_ratios/financial_ratios.js:55
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:198
msgid "Turnover Ratios"
msgstr ""
msgstr "Pokazatelji obrtaja"
#. Option for the 'Frequency To Collect Progress' (Select) field in DocType
#. 'Project'

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2025-10-05 09:35+0000\n"
"PO-Revision-Date: 2025-10-06 10:13\n"
"PO-Revision-Date: 2025-10-11 11:22\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Swedish\n"
"MIME-Version: 1.0\n"
@@ -50930,7 +50930,7 @@ msgstr "Visa Bokföring Register Saldo"
#: erpnext/accounts/report/trial_balance/trial_balance.js:116
msgid "Show Group Accounts"
msgstr "Visa Gruppkonton"
msgstr "Visa Grupp Konto"
#. Label of the show_in_website (Check) field in DocType 'Sales Partner'
#: erpnext/setup/doctype/sales_partner/sales_partner.json
@@ -58449,19 +58449,19 @@ msgstr "Kvalitet Procedur Träd"
#: erpnext/accounts/workspace/accounting/accounting.json
#: erpnext/accounts/workspace/financial_reports/financial_reports.json
msgid "Trial Balance"
msgstr "Aktuell Saldo"
msgstr "Brutto Saldo"
#. Name of a report
#: erpnext/accounts/report/trial_balance_simple/trial_balance_simple.json
msgid "Trial Balance (Simple)"
msgstr "Aktuell Saldo (Enkel)"
msgstr "Brutto Saldo (Enkel)"
#. Name of a report
#. Label of a Link in the Financial Reports Workspace
#: erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.json
#: erpnext/accounts/workspace/financial_reports/financial_reports.json
msgid "Trial Balance for Party"
msgstr "Aktuell Saldo för Parti"
msgstr "Brutto Saldo för Parti"
#. Label of the trial_period_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
@@ -58485,7 +58485,7 @@ msgstr "Prov Period Start Datum kan inte vara efter Prenumeration Start Datum"
#: erpnext/accounts/doctype/subscription/subscription.json
#: erpnext/accounts/doctype/subscription/subscription_list.js:4
msgid "Trialing"
msgstr "Prövning"
msgstr "Prov Period"
#. Description of the 'General Ledger' (Int) field in DocType 'Accounts
#. Settings'
@@ -62170,7 +62170,7 @@ msgstr "Du kan inte göra några ändringar i Jobbkort eftersom Arbetsorder är
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:187
msgid "You can't process the serial number {0} as it has already been used in the SABB {1}. {2} if you want to inward same serial number multiple times then enabled 'Allow existing Serial No to be Manufactured/Received again' in the {3}"
msgstr "Du kan inte behandla serienummer {0} eftersom det redan har använts i Serienummer och Parti Paket {1}. {2} Om du vill leverera in samma serienummer flera gånger aktiverar du \"Tillåt att befintligt serienummer produceras/tas emot igen\" i {3}"
msgstr "Du kan inte behandla serienummer {0} eftersom det redan har använts i Serienummer och Parti Paket {1}. {2} Om du vill leverera in samma serienummer flera gånger aktiverar du \"Tillåt att befintligt serienummer Produceras/Tas Emot igen\" i {3}"
#: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:192
msgid "You can't redeem Loyalty Points having more value than the Total Amount."

View File

@@ -220,9 +220,9 @@ class BOM(WebsiteGenerator):
def onload(self):
super().onload()
self.set_onload_for_muulti_level_bom()
self.set_onload_for_multi_level_bom()
def set_onload_for_muulti_level_bom(self):
def set_onload_for_multi_level_bom(self):
use_multi_level_bom = frappe.db.get_value(
"Property Setter",
{"field_name": "use_multi_level_bom", "doc_type": "Work Order", "property": "default"},

View File

@@ -34,6 +34,7 @@ frappe.ui.form.on("Production Plan", {
query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
filters: {
company: frm.doc.company,
item_code: frm.doc.item_code,
},
};
});
@@ -115,6 +116,8 @@ frappe.ui.form.on("Production Plan", {
__("View")
);
let has_create_buttons = false;
if (frm.doc.status !== "Completed") {
if (frm.doc.status === "Closed") {
frm.add_custom_button(
@@ -144,6 +147,7 @@ frappe.ui.form.on("Production Plan", {
},
__("Create")
);
has_create_buttons = true;
}
if (
@@ -158,10 +162,11 @@ frappe.ui.form.on("Production Plan", {
},
__("Create")
);
has_create_buttons = true;
}
}
if (frm.doc.status !== "Closed") {
if (has_create_buttons && frm.doc.status !== "Closed") {
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
}

View File

@@ -936,7 +936,7 @@ class ProductionPlan(Document):
material_request_type = item.material_request_type or item_doc.default_material_request_type
# key for Sales Order:Material Request Type:Customer
key = "{}:{}:{}".format(item.sales_order, material_request_type, item_doc.customer or "")
key = "{}:{}:{}".format(item.sales_order, material_request_type, "")
schedule_date = item.schedule_date or add_days(nowdate(), cint(item_doc.lead_time_days))
if key not in material_request_map:
@@ -949,7 +949,6 @@ class ProductionPlan(Document):
"status": "Draft",
"company": self.company,
"material_request_type": material_request_type,
"customer": item_doc.customer or "",
}
)
material_request_list.append(material_request)
@@ -965,6 +964,7 @@ class ProductionPlan(Document):
if material_request_type == "Material Transfer"
else None,
"qty": item.quantity - item.requested_qty,
"uom": item.uom,
"schedule_date": schedule_date,
"warehouse": item.warehouse,
"sales_order": item.sales_order,
@@ -1624,6 +1624,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
include_safety_stock = doc.get("include_safety_stock")
so_item_details = frappe._dict()
existing_sub_assembly_items = set()
sub_assembly_items = defaultdict(int)
if doc.get("skip_available_sub_assembly_item") and doc.get("sub_assembly_items"):
@@ -1659,7 +1660,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
and doc.get("sub_assembly_items")
):
item_details = get_raw_materials_of_sub_assembly_items(
so_item_details[doc.get("sales_order")].keys() if so_item_details else [],
existing_sub_assembly_items,
item_details,
company,
bom_no,
@@ -1927,7 +1928,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(Sum(child.required_bom_qty))
.select(Sum(child.quantity * child.conversion_factor))
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
@@ -2044,6 +2045,7 @@ def get_raw_materials_of_sub_assembly_items(
sub_assembly_items,
planned_qty=planned_qty,
)
existing_sub_assembly_items.add(item.item_code)
else:
if not item.conversion_factor and item.purchase_uom:
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
@@ -2081,6 +2083,9 @@ def sales_order_query(doctype=None, txt=None, searchfield=None, start=None, page
if filters.get("sales_orders"):
query = query.where(so_table.name.isin(filters.get("sales_orders")))
if filters.get("item_code"):
query = query.where(table.item_code == filters.get("item_code"))
if txt:
query = query.where(table.parent.like(f"%{txt}%"))

View File

@@ -692,7 +692,6 @@ class TestProductionPlan(IntegrationTestCase):
mr = frappe.get_doc("Material Request", material_request)
self.assertTrue(mr.material_request_type, "Customer Provided")
self.assertTrue(mr.customer, "_Test Customer")
def test_production_plan_with_multi_level_bom(self):
"""
@@ -1256,7 +1255,6 @@ class TestProductionPlan(IntegrationTestCase):
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty, before_qty)
completed_plans = get_non_completed_production_plans()
for plan in plans:
self.assertFalse(plan in completed_plans)
@@ -1859,11 +1857,17 @@ class TestProductionPlan(IntegrationTestCase):
def test_calculation_of_sub_assembly_items(self):
make_item("Sub Assembly Item ", properties={"is_stock_item": 1})
make_item("Sub Assembly Item 2", properties={"is_stock_item": 1})
make_item("RM Item 1", properties={"is_stock_item": 1})
make_item("RM Item 2", properties={"is_stock_item": 1})
make_item("_Test FG Item 3", properties={"is_stock_item": 1})
make_item("_Test FG Item 4", properties={"is_stock_item": 1})
make_bom(item="Sub Assembly Item", raw_materials=["RM Item 1", "RM Item 2"])
make_bom(item="Sub Assembly Item 2", raw_materials=["RM Item 2"])
make_bom(item="_Test FG Item", raw_materials=["Sub Assembly Item", "RM Item 1"])
make_bom(item="_Test FG Item 2", raw_materials=["Sub Assembly Item"])
make_bom(item="_Test FG Item 3", raw_materials=["RM Item 1"])
make_bom(item="_Test FG Item 4", raw_materials=["Sub Assembly Item 2"])
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -1899,13 +1903,40 @@ class TestProductionPlan(IntegrationTestCase):
"warehouse": "_Test Warehouse - _TC",
},
)
# Assembly item with similar RM item
plan.append(
"po_items",
{
"use_multi_level_bom": 1,
"item_code": "_Test FG Item 3",
"bom_no": frappe.db.get_value("Item", "_Test FG Item 3", "default_bom"),
"planned_qty": 10,
"planned_start_date": now_datetime(),
"stock_uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
},
)
# Sub-assembly item with similar RM item
plan.append(
"po_items",
{
"use_multi_level_bom": 1,
"item_code": "_Test FG Item 4",
"bom_no": frappe.db.get_value("Item", "_Test FG Item 4", "default_bom"),
"planned_qty": 10,
"planned_start_date": now_datetime(),
"stock_uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
},
)
plan.save()
plan.ignore_existing_ordered_qty = 1
plan.get_sub_assembly_items()
self.assertEqual(plan.sub_assembly_items[0].qty, 20)
self.assertEqual(plan.sub_assembly_items[1].qty, 50)
self.assertEqual(plan.sub_assembly_items[0].qty, 20) # Sub Assembly For FG 1
self.assertEqual(plan.sub_assembly_items[1].qty, 50) # Sub Assembly For FG 2
self.assertEqual(plan.sub_assembly_items[2].qty, 10) # Sub Assembly For FG 4
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
@@ -1913,8 +1944,11 @@ class TestProductionPlan(IntegrationTestCase):
mr_items = get_items_for_material_requests(plan.as_dict())
self.assertEqual(mr_items[0].get("quantity"), 80)
self.assertEqual(mr_items[1].get("quantity"), 70)
# RM Item 1 (FG1 (100 + 100) + FG2 (50) + FG3 (10) - 90 in stock - 80 sub assembly stock)
self.assertEqual(mr_items[0].get("quantity"), 90)
# RM Item 2 (FG1 (100) + FG2 (50) + FG4 (10) - 80 sub assembly stock)
self.assertEqual(mr_items[1].get("quantity"), 80)
def test_stock_reservation_against_production_plan(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
@@ -2489,4 +2523,7 @@ def make_bom(**args):
if not args.do_not_submit:
bom.submit()
if args.set_as_default_bom and not args.do_not_save and not args.do_not_submit:
frappe.set_value("Item", args.item, "default_bom", bom.name)
return bom

View File

@@ -767,12 +767,24 @@ erpnext.work_order = {
frm.add_custom_button(
__("Additional Material Transfer"),
function () {
erpnext.work_order.make_se(
frm,
"Material Transfer for Manufacture",
qty,
1
);
let purpose = "Material Transfer for Manufacture";
erpnext.work_order
.show_prompt_for_qty_input(frm, purpose, qty, 1)
.then((data) => {
return frappe.xcall(
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
{
work_order_id: frm.doc.name,
purpose: purpose,
qty: data.qty,
is_additional_transfer_entry: 1,
}
);
})
.then((stock_entry) => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
});
},
__("Make")
);
@@ -940,8 +952,8 @@ erpnext.work_order = {
return flt(max, precision("qty"));
},
show_prompt_for_qty_input: function (frm, purpose) {
let max = this.get_max_transferable_qty(frm, purpose);
show_prompt_for_qty_input: function (frm, purpose, qty, additional_transfer_entry) {
let max = !additional_transfer_entry ? this.get_max_transferable_qty(frm, purpose) : qty;
let fields = [
{
@@ -951,7 +963,10 @@ erpnext.work_order = {
description: __("Max: {0}", [max]),
default: max,
},
{
];
if (!additional_transfer_entry) {
fields.push({
fieldtype: "Check",
label: __("Consider Process Loss"),
fieldname: "consider_process_loss",
@@ -963,8 +978,8 @@ erpnext.work_order = {
frm.qty_prompt.set_value("qty", max);
}
},
},
];
});
}
if (purpose === "Disassemble") {
fields.push({

View File

@@ -15,6 +15,8 @@
"image",
"bom_no",
"mps",
"subcontracting_inward_order",
"subcontracting_inward_order_item",
"sales_order",
"column_break1",
"company",
@@ -23,6 +25,7 @@
"track_semi_finished_goods",
"reserve_stock",
"column_break_agjv",
"max_producible_qty",
"material_transferred_for_manufacturing",
"additional_transferred_qty",
"produced_qty",
@@ -154,6 +157,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.subcontracting_inward_order",
"fieldname": "allow_alternative_item",
"fieldtype": "Check",
"label": "Allow Alternative Item"
@@ -164,7 +168,8 @@
"fieldname": "use_multi_level_bom",
"fieldtype": "Check",
"label": "Use Multi-Level BOM",
"print_hide": 1
"print_hide": 1,
"read_only_depends_on": "eval:doc.subcontracting_inward_order"
},
{
"default": "0",
@@ -219,6 +224,7 @@
"read_only": 1
},
{
"depends_on": "eval:!doc.subcontracting_inward_order",
"fieldname": "sales_order",
"fieldtype": "Link",
"in_global_search": 1,
@@ -235,7 +241,7 @@
},
{
"default": "0",
"depends_on": "skip_transfer",
"depends_on": "eval:doc.skip_transfer && !doc.subcontracting_inward_order",
"fieldname": "from_wip_warehouse",
"fieldtype": "Check",
"label": "Backflush Raw Materials From Work-in-Progress Warehouse"
@@ -247,6 +253,7 @@
"options": "fa fa-building"
},
{
"depends_on": "eval:!(doc.skip_transfer && doc.subcontracting_inward_order)",
"description": "This is a location where operations are executed.",
"fieldname": "wip_warehouse",
"fieldtype": "Link",
@@ -259,7 +266,8 @@
"fieldname": "fg_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"read_only_depends_on": "subcontracting_inward_order"
},
{
"fieldname": "column_break_12",
@@ -418,6 +426,7 @@
"width": "50%"
},
{
"depends_on": "eval:!doc.subcontracting_inward_order",
"description": "Manufacture against Material Request",
"fieldname": "material_request",
"fieldtype": "Link",
@@ -495,7 +504,8 @@
"fieldname": "source_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"read_only_depends_on": "eval:doc.subcontracting_inward_order"
},
{
"description": "In Mins",
@@ -595,7 +605,8 @@
"default": "0",
"fieldname": "reserve_stock",
"fieldtype": "Check",
"label": " Reserve Stock"
"label": "Reserve Stock",
"read_only_depends_on": "subcontracting_inward_order"
},
{
"depends_on": "eval:doc.docstatus==1",
@@ -622,6 +633,32 @@
"label": "Additional Transferred Qty",
"no_copy": 1,
"read_only": 1
},
{
"depends_on": "subcontracting_inward_order",
"fieldname": "subcontracting_inward_order",
"fieldtype": "Link",
"label": "Subcontracting Inward Order",
"options": "Subcontracting Inward Order",
"read_only": 1
},
{
"fieldname": "subcontracting_inward_order_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Subcontracting Inward Order Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "max_producible_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Max Producible Qty",
"no_copy": 1,
"non_negative": 1,
"read_only": 1
}
],
"grid_page_length": 50,
@@ -630,7 +667,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2025-09-29 15:57:47.022616",
"modified": "2025-10-12 14:24:57.699749",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",

View File

@@ -102,6 +102,7 @@ class WorkOrder(Document):
material_request: DF.Link | None
material_request_item: DF.Data | None
material_transferred_for_manufacturing: DF.Float
max_producible_qty: DF.Float
mps: DF.Link | None
naming_series: DF.Literal["MFG-WO-.YYYY.-"]
operations: DF.Table[WorkOrderOperation]
@@ -138,6 +139,8 @@ class WorkOrder(Document):
"Cancelled",
]
stock_uom: DF.Link | None
subcontracting_inward_order: DF.Link | None
subcontracting_inward_order_item: DF.Data | None
total_operating_cost: DF.Currency
track_semi_finished_goods: DF.Check
transfer_material_against: DF.Literal["", "Work Order", "Job Card"]
@@ -180,7 +183,11 @@ class WorkOrder(Document):
if self.bom_no:
validate_bom_no(self.production_item, self.bom_no)
self.validate_sales_order()
if not self.subcontracting_inward_order:
self.validate_sales_order()
else:
self.validate_self_rm_warehouse()
self.set_default_warehouse()
self.validate_warehouse_belongs_to_company()
self.check_wip_warehouse_skip()
@@ -203,6 +210,7 @@ class WorkOrder(Document):
self.set_required_items(reset_only_qty=len(self.get("required_items")))
self.enable_auto_reserve_stock()
self.validate_operations_sequence()
self.validate_subcontracting_inward_order()
def validate_dates(self):
if self.actual_start_date and self.actual_end_date:
@@ -210,7 +218,7 @@ class WorkOrder(Document):
frappe.throw(_("Actual End Date cannot be before Actual Start Date"))
def validate_fg_warehouse_for_reservation(self):
if self.reserve_stock and self.sales_order:
if self.reserve_stock and self.sales_order and not self.subcontracting_inward_order:
warehouses = frappe.get_all(
"Sales Order Item",
filters={"parent": self.sales_order, "item_code": self.production_item},
@@ -257,6 +265,24 @@ class WorkOrder(Document):
)
sequence_id = op.sequence_id
def validate_subcontracting_inward_order(self):
if scio := self.subcontracting_inward_order:
if self.source_warehouse != (
rm_receipt_warehouse := frappe.get_cached_value(
"Subcontracting Inward Order",
scio,
"customer_warehouse",
)
):
frappe.throw(
_(
"Source Warehouse {0} must be same as Customer Warehouse {1} in the Subcontracting Inward Order"
).format(
frappe.bold(self.source_warehouse),
frappe.bold(rm_receipt_warehouse),
)
)
def set_warehouses(self):
for row in self.required_items:
if not row.source_warehouse:
@@ -330,6 +356,15 @@ class WorkOrder(Document):
else:
frappe.throw(_("Sales Order {0} is not valid").format(self.sales_order))
def validate_self_rm_warehouse(self):
for item in [item for item in self.required_items if not item.is_customer_provided_item]:
if frappe.get_cached_value("Warehouse", item.source_warehouse, "customer"):
frappe.throw(
_("Row #{0}: Source Warehouse {1} for item {2} cannot be a customer warehouse.").format(
item.idx, frappe.bold(item.source_warehouse), frappe.bold(item.item_code)
)
)
def check_sales_order_on_hold_or_close(self):
status = frappe.db.get_value("Sales Order", self.sales_order, "status")
if status in ("Closed", "On Hold"):
@@ -636,6 +671,8 @@ class WorkOrder(Document):
if self.reserve_stock:
self.update_stock_reservation()
self.update_subcontracting_inward_order_received_items()
def on_cancel(self):
self.validate_cancel()
self.db_set("status", "Cancelled")
@@ -657,10 +694,68 @@ class WorkOrder(Document):
if self.reserve_stock:
self.update_stock_reservation()
self.update_subcontracting_inward_order_received_items()
def update_stock_reservation(self):
self.set_qty_change()
make_stock_reservation_entries(self)
self.db_set("status", self.get_status())
def set_qty_change(self):
if scio_item_name := self.get("subcontracting_inward_order_item"):
scio_rm_item_names = frappe.db.get_all(
"Subcontracting Inward Order Received Item",
filters={"reference_name": scio_item_name, "docstatus": 1, "is_customer_provided_item": 1},
pluck="name",
)
self.qty_change = frappe._dict()
data = frappe.get_all(
"Subcontracting Inward Order Received Item",
{"name": ["in", scio_rm_item_names]},
["rm_item_code", "required_qty as bom_qty", "work_order_qty", "received_qty"],
)
for d in data:
wo_item = next(
wo_item for wo_item in self.get("required_items") if wo_item.item_code == d.rm_item_code
)
if (
d.work_order_qty + (wo_item.required_qty if self._action == "submit" else 0)
) == d.bom_qty and d.received_qty > d.bom_qty:
self.qty_change[wo_item.name] = d.received_qty - d.bom_qty
def update_subcontracting_inward_order_received_items(self):
if scio_item_name := self.get("subcontracting_inward_order_item"):
scio_rm_data = frappe.get_all(
"Subcontracting Inward Order Received Item",
filters={"reference_name": scio_item_name, "docstatus": 1},
fields=["name", "rm_item_code"],
)
required_qty = {
wo_item.item_code: wo_item.required_qty
for wo_item in self.get("required_items")
if wo_item.item_code in [d.rm_item_code for d in scio_rm_data]
}
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
case_expr = Case()
for item in scio_rm_data:
case_expr = case_expr.when(
table.rm_item_code == item.rm_item_code,
table.work_order_qty
+ (
required_qty[item.rm_item_code]
if self._action == "submit"
else -required_qty[item.rm_item_code]
),
)
frappe.qb.update(table).set(table.work_order_qty, case_expr).where(
(table.name.isin([d.name for d in scio_rm_data])) & (table.docstatus == 1)
).run()
def create_serial_no_batch_no(self):
if not (self.has_serial_no or self.has_batch_no):
return
@@ -1229,6 +1324,15 @@ class WorkOrder(Document):
OverProductionError,
)
if self.subcontracting_inward_order and self.qty > self.max_producible_qty:
frappe.msgprint(
_(
"Warning: Quantity exceeds maximum producible quantity based on quantity of raw materials received through the Subcontracting Inward Order {0}."
).format(frappe.bold(self.subcontracting_inward_order)),
alert=True,
indicator="orange",
)
def validate_transfer_against(self):
if self.docstatus != 1:
# let user configure operations until they're ready to submit
@@ -1330,12 +1434,20 @@ class WorkOrder(Document):
},
)
if self.subcontracting_inward_order and not frappe.get_cached_value(
"Item", item.item_code, "is_customer_provided_item"
):
self.required_items[-1].source_warehouse = item.default_warehouse
if not self.project:
self.project = item.get("project")
self.set_available_qty()
def update_transferred_qty_for_required_items(self):
if self.skip_transfer:
return
ste = frappe.qb.DocType("Stock Entry")
ste_child = frappe.qb.DocType("Stock Entry Detail")
@@ -1529,7 +1641,7 @@ class WorkOrder(Document):
stock_entry.reload()
if stock_entry.purpose == "Manufacture" and (
self.sales_order or self.production_plan_sub_assembly_item
self.sales_order or self.production_plan_sub_assembly_item or self.subcontracting_inward_order
):
items = self.get_finished_goods_for_reservation(stock_entry)
elif stock_entry.purpose == "Material Transfer for Manufacture":
@@ -1574,6 +1686,8 @@ class WorkOrder(Document):
if self.production_plan_sub_assembly_item:
# Reserve the sub-assembly item for the final product for the work order.
item_details = self.get_wo_details()
elif self.subcontracting_inward_order:
item_details = self.get_scio_details()
else:
# Reserve the final product for the sales order.
item_details = self.get_so_details()
@@ -1662,6 +1776,25 @@ class WorkOrder(Document):
return query.run(as_dict=1)
def get_scio_details(self):
return frappe.get_all(
"Subcontracting Inward Order Item",
filters={
"name": self.subcontracting_inward_order_item,
"docstatus": 1,
},
fields=[
"item_code",
"name",
"qty as stock_qty",
"produced_qty as stock_reserved_qty",
"delivery_warehouse as warehouse",
"parent as voucher_no",
"parenttype as voucher_type",
"delivered_qty",
],
)
def get_so_details(self):
return frappe.get_all(
"Sales Order Item",
@@ -1764,7 +1897,8 @@ class WorkOrder(Document):
@frappe.whitelist()
def make_stock_reservation_entries(doc, items=None, table_name=None, is_transfer=True, notify=False):
def make_stock_reservation_entries(doc, items=None, is_transfer=True, notify=False):
is_transfer = cint(is_transfer)
if isinstance(doc, str):
doc = parse_json(doc)
doc = frappe.get_doc("Work Order", doc.get("name"))
@@ -1778,6 +1912,14 @@ def make_stock_reservation_entries(doc, items=None, table_name=None, is_transfer
sre.transfer_reservation_entries_to(
doc.production_plan, from_doctype="Production Plan", to_doctype="Work Order"
)
elif doc.subcontracting_inward_order and is_transfer:
sre.transfer_reservation_entries_to(
doc.subcontracting_inward_order,
from_doctype="Subcontracting Inward Order",
to_doctype="Work Order",
against_fg_item=doc.subcontracting_inward_order_item,
qty_change=doc.qty_change,
)
else:
sre_created = sre.make_stock_reservation_entries()
if sre_created:
@@ -2042,6 +2184,9 @@ def make_stock_entry(
qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty))
)
if purpose == "Manufacture" and work_order.subcontracting_inward_order:
stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order
if work_order.bom_no:
stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required")

View File

@@ -29,6 +29,7 @@
"column_break_jash",
"stock_reserved_qty",
"is_additional_item",
"is_customer_provided_item",
"voucher_detail_reference"
],
"fields": [
@@ -52,7 +53,8 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Source Warehouse",
"options": "Warehouse"
"options": "Warehouse",
"read_only_depends_on": "eval:parent.subcontracting_inward_order && doc.is_customer_provided_item"
},
{
"fieldname": "column_break_3",
@@ -91,6 +93,7 @@
},
{
"default": "0",
"depends_on": "eval:!parent.subcontracting_inward_order",
"fieldname": "allow_alternative_item",
"fieldtype": "Check",
"label": "Allow Alternative Item"
@@ -190,12 +193,20 @@
"label": "Voucher Detail Reference",
"no_copy": 1,
"read_only": 1
},
{
"default": "0",
"fetch_from": "item_code.is_customer_provided_item",
"fieldname": "is_customer_provided_item",
"fieldtype": "Check",
"label": "Is Customer Provided Item",
"read_only": 1
}
],
"grid_page_length": 50,
"istable": 1,
"links": [],
"modified": "2025-05-12 17:36:00.115181",
"modified": "2025-10-12 14:27:16.721532",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order Item",

View File

@@ -23,6 +23,7 @@ class WorkOrderItem(Document):
description: DF.Text | None
include_item_in_manufacturing: DF.Check
is_additional_item: DF.Check
is_customer_provided_item: DF.Check
item_code: DF.Link | None
item_name: DF.Data | None
operation: DF.Link | None

View File

@@ -113,6 +113,13 @@ class ProductionPlanReport:
self.orders = query.run(as_dict=True)
def get_raw_materials(self):
"""Retrieve raw materials and source warehouses for production orders.
This method collects BOM or Work Order items depending on the selected
filter and updates `self.raw_materials_dict`, `self.warehouses`,
and `self.item_codes` accordingly.
"""
if not self.orders:
return
self.warehouses = [d.warehouse for d in self.orders]
@@ -135,7 +142,7 @@ class ProductionPlanReport:
)
or []
)
self.warehouses.extend([d.source_warehouse for d in raw_materials])
self.warehouses.extend([d.warehouse for d in raw_materials])
else:
bom_nos = []

View File

@@ -440,3 +440,5 @@ erpnext.patches.v16_0.make_workstation_operating_components #1
erpnext.patches.v16_0.set_reporting_currency
erpnext.patches.v16_0.set_posting_datetime_for_sabb_and_drop_indexes
erpnext.patches.v16_0.update_serial_no_reference_name
erpnext.patches.v16_0.rename_subcontracted_quantity
erpnext.patches.v16_0.add_new_stock_entry_types

View File

@@ -0,0 +1,14 @@
import frappe
def execute():
for stock_entry_type in [
"Receive from Customer",
"Return Raw Material to Customer",
"Subcontracting Delivery",
"Subcontracting Return",
]:
if not frappe.db.exists("Stock Entry Type", stock_entry_type):
frappe.new_doc("Stock Entry Type", purpose=stock_entry_type, is_standard=1).insert(
set_name=stock_entry_type, ignore_permissions=True
)

View File

@@ -0,0 +1,7 @@
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if frappe.db.has_column("Purchase Order Item", "subcontracted_quantity"):
rename_field("Purchase Order Item", "subcontracted_quantity", "subcontracted_qty")

View File

@@ -4,6 +4,9 @@ import frappe
def execute():
# Update the reference_name, reference_doctype fields for Serial No where it is null
if not frappe.db.has_column("Serial and Batch Bundle", "posting_date"):
return
sabb = frappe.qb.DocType("Serial and Batch Bundle")
sabb_entry = frappe.qb.DocType("Serial and Batch Entry")
serial_no = frappe.qb.DocType("Serial No").as_("sn")
@@ -16,7 +19,7 @@ def execute():
.on(sabb.name == sabb_entry.parent)
.set(serial_no.reference_name, serial_no.purchase_document_no)
.set(serial_no.reference_doctype, sabb.voucher_type)
.set(serial_no.posting_date, sabb.posting_datetime)
.set(serial_no.posting_date, sabb.posting_date)
.where(
(sabb.voucher_no == serial_no.purchase_document_no)
& (sabb.is_cancelled == 0)

View File

@@ -1,15 +1,17 @@
import urllib.parse
import frappe
def get_context(context):
if frappe.form_dict.project:
context.parents = [
{"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project}
]
context.success_url = "/projects?project=" + frappe.form_dict.project
if project := frappe.form_dict.project:
title = frappe.utils.data.escape_html(project)
route = "/projects?" + urllib.parse.urlencode({"project": project})
context.parents = [{"title": title, "route": route}]
context.success_url = route
elif context.doc and context.doc.get("project"):
context.parents = [
{"title": context.doc.project, "route": "/projects?project=" + context.doc.project}
]
context.success_url = "/projects?project=" + context.doc.project
elif context.doc and (project := context.doc.get("project")):
title = frappe.utils.data.escape_html(project)
route = "/projects?" + urllib.parse.urlencode({"project": project})
context.parents = [{"title": title, "route": route}]
context.success_url = route

View File

@@ -174,13 +174,15 @@ erpnext.buying = {
shipping_address: this.frm.doc.shipping_address,
},
callback: (r) => {
this.frm.set_value("billing_address", r.message.primary_address || "");
if (!this.frm.doc.billing_address)
this.frm.set_value("billing_address", r.message.primary_address || "");
if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
this.frm.set_value(
"shipping_address",
r.message.shipping_address || this.frm.doc.shipping_address || ""
);
if (
!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") ||
this.frm.doc.shipping_address
)
return;
this.frm.set_value("shipping_address", r.message.shipping_address || "");
},
});
erpnext.utils.set_letter_head(this.frm);

View File

@@ -1170,7 +1170,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (
frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype)
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) &&
!this.frm.doc.shipping_address
) {
let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
@@ -1692,6 +1693,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
);
var company_currency = this.get_company_currency();
if (
this._last_company_currency === company_currency &&
this._last_price_list_currency === this.frm.doc.price_list_currency
) {
return;
}
this._last_company_currency = company_currency;
this._last_price_list_currency = this.frm.doc.price_list_currency;
this.change_form_labels(company_currency);
this.change_grid_labels(company_currency);
this.frm.refresh_fields();

145
erpnext/public/js/print.js Normal file
View File

@@ -0,0 +1,145 @@
let beforePrintHandled = false;
frappe.realtime.on("sales_invoice_before_print", (data) => {
const route = frappe.get_route();
if (!beforePrintHandled && route[0] === "print" && route[1] === "Sales Invoice") {
beforePrintHandled = true;
let companyDetailsDialog = new frappe.ui.Dialog({
title: "Enter Company Details",
fields: [
{
label: "Company Logo",
fieldname: "company_logo",
fieldtype: "Attach Image",
reqd: data.company_logo ? 0 : 1,
hidden: data.company_logo ? 1 : 0,
},
{
label: "Website",
fieldname: "website",
fieldtype: "Data",
hidden: data.website ? 1 : 0,
},
{
label: "Phone No",
fieldname: "phone_no",
fieldtype: "Data",
reqd: data.phone_no ? 0 : 1,
hidden: data.phone_no ? 1 : 0,
},
{
label: "Email",
fieldname: "email",
fieldtype: "Data",
options: "Email",
reqd: data.email ? 0 : 1,
hidden: data.email ? 1 : 0,
},
{
fieldname: "section_break_1",
fieldtype: "Section Break",
},
{
label: "Address Title",
fieldname: "address_title",
fieldtype: "Data",
reqd: data.address_line ? 0 : 1,
hidden: data.address_line ? 1 : 0,
},
{
label: "Address Type",
fieldname: "address_type",
fieldtype: "Select",
options: ["Billing", "Shipping"],
default: "Billing",
reqd: data.address_line ? 0 : 1,
hidden: data.address_line ? 1 : 0,
},
{
label: "Address Line 1",
fieldname: "address_line1",
fieldtype: "Data",
reqd: data.address_line ? 0 : 1,
hidden: data.address_line ? 1 : 0,
},
{
label: "Address Line 2",
fieldname: "address_line2",
fieldtype: "Data",
hidden: data.address_line ? 1 : 0,
},
{
label: "City",
fieldname: "city",
fieldtype: "Data",
reqd: data.address_line ? 0 : 1,
hidden: data.address_line ? 1 : 0,
},
{
label: "State",
fieldname: "state",
fieldtype: "Data",
hidden: data.address_line ? 1 : 0,
},
{
label: "Country",
fieldname: "country",
fieldtype: "Link",
options: "Country",
reqd: data.address_line ? 0 : 1,
hidden: data.address_line ? 1 : 0,
},
{
label: "Postal Code",
fieldname: "pincode",
fieldtype: "Data",
hidden: data.address_line ? 1 : 0,
},
{
label: "Select Company Address",
fieldname: "company_address",
fieldtype: "Link",
options: "Address",
get_query: function () {
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: {
link_doctype: "Company",
link_name: data.company,
},
};
},
reqd: data.address_line && !data.company_address ? 1 : 0,
hidden: data.address_line && !data.company_address ? 0 : 1,
},
],
primary_action_label: "Save",
primary_action(values) {
frappe.call({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.save_company_master_details",
args: {
name: data.name,
company: data.company,
details: values,
},
callback: function () {
companyDetailsDialog.hide();
frappe.msgprint(__("Updating details."));
setTimeout(() => {
window.location.reload();
}, 1000);
},
});
},
});
companyDetailsDialog.show();
}
});
frappe.router.on("change", () => {
const route = frappe.get_route();
if (route[0] !== "print" || route[1] !== "Sales Invoice") {
beforePrintHandled = false;
}
});

View File

@@ -195,6 +195,7 @@ $.extend(erpnext.stock_reservation, {
args: {
doc: frm.doc,
items: data.items,
is_transfer: 0,
table_name: table_name,
notify: true,
},

View File

@@ -5,7 +5,7 @@
<span class="workstation-status-title" style="font-size:10px">{{row.status}}</span>
</span>
</div>
<div class="workstation-image {{row.workstation_off}}" onclick="location.href='{{row.workstation_link}}'">
<div class="workstation-image {{row.workstation_off}}" onclick="location.href='{{ frappe.utils.get_form_link('Workstation', row.name)}}'">
<div class="workstation-image-container flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
{% if(row.status_image) { %}
<img class="workstation-image-cls" src="{{row.status_image}}">

View File

@@ -645,7 +645,7 @@ erpnext.utils.update_child_items = function (opts) {
get_query: function () {
let filters;
if (frm.doc.doctype == "Sales Order") {
filters = { is_sales_item: 1 };
filters = { is_sales_item: 1, is_stock_item: !frm.doc.is_subcontracted };
} else if (frm.doc.doctype == "Purchase Order") {
if (frm.doc.is_subcontracted) {
if (frm.doc.is_old_subcontracting_flow) {
@@ -801,7 +801,7 @@ erpnext.utils.update_child_items = function (opts) {
}
if (
frm.doc.doctype == "Purchase Order" &&
["Purchase Order", "Sales Order"].includes(frm.doc.doctype) &&
frm.doc.is_subcontracted &&
!frm.doc.is_old_subcontracting_flow
) {
@@ -857,7 +857,7 @@ erpnext.utils.update_child_items = function (opts) {
},
],
primary_action: function () {
if (frm.doctype == "Sales Order" && has_reserved_stock) {
if (frm.doctype == "Sales Order" && has_reserved_stock && frm.doc.is_subcontracted == 0) {
this.hide();
frappe.confirm(
__(

View File

@@ -113,6 +113,7 @@ erpnext.sales_common = {
);
this.toggle_editable_price_list_rate();
this.change_warehouse_labels_for_return();
}
company() {
@@ -504,6 +505,33 @@ erpnext.sales_common = {
this.frm.set_value("discount_amount", 0);
this.frm.set_value("additional_discount_percentage", 0);
}
is_return() {
let reset = !this.frm.doc.is_return;
this.change_warehouse_labels_for_return(reset);
}
change_warehouse_labels_for_return(reset) {
// swap source and target warehouse labels for return
let source_warehouse_label = __("Source Warehouse");
let target_warehouse_label = __("Set Target Warehouse");
if (this.frm.doc.doctype == "Delivery Note") {
source_warehouse_label = __("Set Source Warehouse");
}
if (reset) {
// reset to original labels
this.frm.set_df_property("set_warehouse", "label", source_warehouse_label);
this.frm.set_df_property("set_target_warehouse", "label", target_warehouse_label);
return;
}
if (this.frm.doc.is_return) {
this.frm.set_df_property("set_warehouse", "label", target_warehouse_label);
this.frm.set_df_property("set_target_warehouse", "label", source_warehouse_label);
}
}
};
},
};

View File

@@ -457,7 +457,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
(["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype) &&
!this.frm.doc.is_return) ||
(this.frm.doc.doctype === "Stock Entry" &&
this.frm.doc.purpose === "Material Receipt")
(this.frm.doc.purpose === "Material Receipt" ||
(this.frm.doc.purpose === "Manufacture" && this.item.is_finished_item)))
) {
is_inward = true;
}
@@ -542,6 +543,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
based_on: based_on,
posting_date: this.frm.doc.posting_date,
posting_time: this.frm.doc.posting_time,
scio_detail: this.item.scio_detail,
},
callback: (r) => {
if (r.message) {

View File

@@ -44,6 +44,15 @@ frappe.ui.form.on("Sales Order", {
};
});
frm.set_query("sales_person", "sales_team", function () {
return {
filters: {
is_group: 0,
enabled: 1,
},
};
});
frm.set_df_property("packed_items", "cannot_add_rows", true);
frm.set_df_property("packed_items", "cannot_delete_rows", true);
},
@@ -54,7 +63,8 @@ frappe.ui.form.on("Sales Order", {
frm.doc.status !== "Closed" &&
flt(frm.doc.per_delivered) < 100 &&
flt(frm.doc.per_billed) < 100 &&
frm.has_perm("write")
frm.has_perm("write") &&
!frm.doc.is_subcontracted
) {
frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
@@ -84,7 +94,8 @@ frappe.ui.form.on("Sales Order", {
if (
frm.doc.__onload &&
frm.doc.__onload.has_reserved_stock &&
frappe.model.can_cancel("Stock Reservation Entry")
frappe.model.can_cancel("Stock Reservation Entry") &&
!frm.doc.is_subcontracted
) {
frm.add_custom_button(
__("Unreserve"),
@@ -93,16 +104,21 @@ frappe.ui.form.on("Sales Order", {
);
}
frm.doc.items.forEach((item) => {
if (flt(item.stock_reserved_qty) > 0 && frappe.model.can_read("Stock Reservation Entry")) {
frm.add_custom_button(
__("Reserved Stock"),
() => frm.events.show_reserved_stock(frm),
__("Stock Reservation")
);
return;
}
});
if (!frm.doc.is_subcontracted) {
frm.doc.items.forEach((item) => {
if (
flt(item.stock_reserved_qty) > 0 &&
frappe.model.can_read("Stock Reservation Entry")
) {
frm.add_custom_button(
__("Reserved Stock"),
() => frm.events.show_reserved_stock(frm),
__("Stock Reservation")
);
return;
}
});
}
}
if (frm.doc.docstatus === 0) {
@@ -112,7 +128,7 @@ frappe.ui.form.on("Sales Order", {
frm.events.get_items_from_internal_purchase_order(frm);
}
if (frm.doc.docstatus === 0) {
if (frm.doc.docstatus === 0 && !frm.doc.is_subcontracted) {
frappe.call({
method: "erpnext.selling.doctype.sales_order.sales_order.get_stock_reservation_status",
callback: function (r) {
@@ -749,10 +765,28 @@ frappe.ui.form.on("Sales Order", {
frm.schedule_dialog.fields_dict.delivery_schedule.refresh();
},
get_subcontracting_boms_for_finished_goods: function (fg_item) {
return frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom.get_subcontracting_boms_for_finished_goods",
args: {
fg_items: fg_item,
},
});
},
get_subcontracting_boms_for_service_item: function (service_item) {
return frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom.get_subcontracting_boms_for_service_item",
args: {
service_item: service_item,
},
});
},
});
frappe.ui.form.on("Sales Order Item", {
item_code: function (frm, cdt, cdn) {
item_code: async function (frm, cdt, cdn) {
var row = locals[cdt][cdn];
if (frm.doc.delivery_date) {
row.delivery_date = frm.doc.delivery_date;
@@ -760,6 +794,50 @@ frappe.ui.form.on("Sales Order Item", {
} else {
frm.script_manager.copy_from_first_row("items", row, ["delivery_date"]);
}
if (frm.doc.is_subcontracted) {
if (row.item_code && !row.fg_item) {
var result = await frm.events.get_subcontracting_boms_for_service_item(row.item_code);
if (result.message && Object.keys(result.message).length) {
var finished_goods = Object.keys(result.message);
// Set FG if only one active Subcontracting BOM is found
if (finished_goods.length === 1) {
row.fg_item = result.message[finished_goods[0]].finished_good;
row.uom = result.message[finished_goods[0]].finished_good_uom;
refresh_field("items");
} else {
const dialog = new frappe.ui.Dialog({
title: __("Select Finished Good"),
size: "small",
fields: [
{
fieldname: "finished_good",
fieldtype: "Autocomplete",
label: __("Finished Good"),
options: finished_goods,
},
],
primary_action_label: __("Select"),
primary_action: () => {
var subcontracting_bom = result.message[dialog.get_value("finished_good")];
if (subcontracting_bom) {
row.fg_item = subcontracting_bom.finished_good;
row.uom = subcontracting_bom.finished_good_uom;
refresh_field("items");
}
dialog.hide();
},
});
dialog.show();
}
}
}
}
},
delivery_date: function (frm, cdt, cdn) {
@@ -782,6 +860,50 @@ frappe.ui.form.on("Sales Order Item", {
},
});
},
fg_item: async function (frm, cdt, cdn) {
if (frm.doc.is_subcontracted) {
var row = locals[cdt][cdn];
if (row.fg_item) {
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item);
if (result.message && Object.keys(result.message).length) {
frappe.model.set_value(cdt, cdn, "item_code", result.message.service_item);
frappe.model.set_value(
cdt,
cdn,
"qty",
flt(row.fg_item_qty) * flt(result.message.conversion_factor)
);
frappe.model.set_value(cdt, cdn, "uom", result.message.service_item_uom);
}
}
}
},
qty: async function (frm, cdt, cdn) {
if (frm.doc.is_subcontracted) {
var row = locals[cdt][cdn];
if (row.fg_item) {
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item);
if (
result.message &&
row.item_code == result.message.service_item &&
row.uom == result.message.service_item_uom
) {
frappe.model.set_value(
cdt,
cdn,
"fg_item_qty",
flt(row.qty) / flt(result.message.conversion_factor)
);
}
}
}
},
});
erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController {
@@ -795,6 +917,22 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
let allow_delivery = false;
if (doc.docstatus == 1) {
if (
!["Closed", "Completed"].includes(doc.status) &&
flt(doc.per_delivered) < 100 &&
flt(doc.per_billed) < 100
) {
if (!doc.__onload || doc.__onload.can_update_items) {
this.frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
frm: this.frm,
child_docname: "items",
child_doctype: "Sales Order Detail",
cannot_add_row: false,
});
});
}
}
if (this.frm.has_perm("submit")) {
if (doc.status === "On Hold") {
// un-hold
@@ -847,11 +985,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
}
if (doc.is_subcontracted) {
if (!doc.items.every((item) => item.qty == item.subcontracted_qty)) {
this.frm.add_custom_button(
__("Subcontracting Inward Order"),
() => {
me.make_subcontracting_inward_order();
},
__("Create")
);
}
}
if (
(!doc.__onload || !doc.__onload.has_reserved_stock) &&
flt(doc.per_picked) < 100 &&
flt(doc.per_delivered) < 100 &&
frappe.model.can_create("Pick List")
frappe.model.can_create("Pick List") &&
!doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Pick List"),
@@ -880,7 +1031,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
);
}
if (frappe.model.can_create("Work Order")) {
if (frappe.model.can_create("Work Order") && !doc.is_subcontracted) {
this.frm.add_custom_button(
__("Work Order"),
() => this.make_work_order(),
@@ -890,7 +1041,10 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// sales invoice
if (flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice")) {
if (
(flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice")) ||
doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Sales Invoice"),
() => me.make_sales_invoice(),
@@ -902,13 +1056,16 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
if (
(!doc.order_type ||
((order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered) < 100)) &&
frappe.model.can_create("Material Request")
frappe.model.can_create("Material Request") &&
!doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Material Request"),
() => this.make_material_request(),
__("Create")
);
if (!doc.is_subcontracted) {
this.frm.add_custom_button(
__("Material Request"),
() => this.make_material_request(),
__("Create")
);
}
this.frm.add_custom_button(
__("Request for Raw Materials"),
() => this.make_raw_material_request(),
@@ -917,7 +1074,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// Make Purchase Order
if (!this.frm.doc.is_internal_customer && frappe.model.can_create("Purchase Order")) {
if (
!this.frm.doc.is_internal_customer &&
frappe.model.can_create("Purchase Order") &&
!doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Purchase Order"),
() => this.make_purchase_order(),
@@ -991,7 +1152,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
}
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Quotation")) {
if (
this.frm.doc.docstatus === 0 &&
frappe.model.can_read("Quotation") &&
!this.frm.doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Quotation"),
function () {
@@ -1011,7 +1176,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
get_query_filters: {
company: me.frm.doc.company,
docstatus: 1,
status: ["!=", "Lost"],
status: ["not in", ["Lost", "Ordered"]],
},
allow_child_item_selection: true,
child_fieldname: "items",
@@ -1606,6 +1771,14 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
},
});
}
make_subcontracting_inward_order() {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_subcontracting_inward_order",
frm: this.frm,
freeze_message: __("Creating Subcontracting Inward Order ..."),
});
}
};
extend_cscript(cur_frm.cscript, new erpnext.selling.SalesOrderController({ frm: cur_frm }));

View File

@@ -25,6 +25,7 @@
"company",
"skip_delivery_note",
"has_unit_price_items",
"is_subcontracted",
"amended_from",
"accounting_dimensions_section",
"cost_center",
@@ -1035,8 +1036,8 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "packed_items",
"depends_on": "packed_items",
"collapsible_depends_on": "eval:!doc.is_subcontracted && doc.packed_items",
"depends_on": "eval:!doc.is_subcontracted && doc.packed_items",
"fieldname": "packing_list",
"fieldtype": "Section Break",
"hide_days": 1,
@@ -1607,7 +1608,7 @@
},
{
"default": "0",
"depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
"depends_on": "eval: ((doc.docstatus == 0 || doc.reserve_stock) && !doc.is_subcontracted)",
"description": "If checked, Stock will be reserved on <b>Submit</b>",
"fieldname": "reserve_stock",
"fieldtype": "Check",
@@ -1688,13 +1689,21 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
},
{
"default": "0",
"fieldname": "is_subcontracted",
"fieldtype": "Check",
"label": "Is Subcontracted",
"print_hide": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2025-07-28 12:14:29.760988",
"modified": "2025-10-12 12:14:29.760988",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -120,6 +120,7 @@ class SalesOrder(SellingController):
incoterm: DF.Link | None
inter_company_order_reference: DF.Link | None
is_internal_customer: DF.Check
is_subcontracted: DF.Check
items: DF.Table[SalesOrderItem]
language: DF.Link | None
letter_head: DF.Link | None
@@ -195,6 +196,10 @@ class SalesOrder(SellingController):
def onload(self) -> None:
super().onload()
if self.get("is_subcontracted"):
self.set_onload("can_update_items", self.can_update_items())
return
if frappe.get_single_value("Stock Settings", "enable_stock_reservation"):
if self.has_unreserved_stock():
self.set_onload("has_unreserved_stock", True)
@@ -202,6 +207,15 @@ class SalesOrder(SellingController):
if has_reserved_stock(self.doctype, self.name):
self.set_onload("has_reserved_stock", True)
def can_update_items(self) -> bool:
result = True
if self.is_subcontracted:
if frappe.db.exists("Subcontracting Inward Order", {"sales_order": self.name, "docstatus": 1}):
result = False
return result
def before_validate(self):
self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items
@@ -233,6 +247,7 @@ class SalesOrder(SellingController):
make_packing_list(self)
self.validate_with_previous_doc()
self.validate_fg_item_for_subcontracting()
self.set_status()
if not self.billing_status:
@@ -243,7 +258,39 @@ class SalesOrder(SellingController):
self.advance_payment_status = "Not Requested"
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.enable_auto_reserve_stock()
if not self.get("is_subcontracted"):
self.enable_auto_reserve_stock()
def validate_fg_item_for_subcontracting(self):
if self.is_subcontracted:
for item in self.items:
if not item.fg_item:
frappe.throw(
_("Row #{0}: Finished Good Item is not specified for service item {1}").format(
item.idx, item.item_code
)
)
else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw(
_("Row #{0}: Finished Good Item {1} must be a sub-contracted item").format(
item.idx, item.fg_item
)
)
if not frappe.db.get_value(
"Subcontracting BOM",
{"finished_good": item.fg_item, "is_active": 1},
"finished_good_bom",
) and not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: BOM not found for FG Item {1}").format(item.idx, item.fg_item)
)
if not item.fg_item_qty:
frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx))
else:
for item in self.items:
item.set("fg_item", None)
item.set("fg_item_qty", 0)
def enable_auto_reserve_stock(self):
if self.is_new() and frappe.get_single_value("Stock Settings", "auto_reserve_stock"):
@@ -449,7 +496,7 @@ class SalesOrder(SellingController):
update_coupon_code_count(self.coupon_code, "used")
if self.get("reserve_stock"):
if self.get("reserve_stock") and not self.get("is_subcontracted"):
self.create_stock_reservation_entries()
def on_cancel(self):
@@ -535,9 +582,23 @@ class SalesOrder(SellingController):
if status == "Draft" and self.docstatus == 1:
self.check_credit_limit()
self.update_reserved_qty()
self.update_subcontracting_order_status()
self.notify_update()
clear_doctype_notifications(self)
def update_subcontracting_order_status(self):
from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import (
update_subcontracting_inward_order_status as update_scio_status,
)
if self.is_subcontracted:
scio = frappe.get_cached_value(
"Subcontracting Inward Order", {"sales_order": self.name, "docstatus": 1}, "name"
)
if scio:
update_scio_status(scio, "Closed" if self.status == "Closed" else None)
def update_reserved_qty(self, so_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""
item_wh_list = []
@@ -1290,6 +1351,46 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
def add_self_rm(doclist):
parent = frappe.qb.DocType("Subcontracting Inward Order")
child = frappe.qb.DocType("Subcontracting Inward Order Received Item")
query = (
frappe.qb.from_(parent)
.join(child)
.on(parent.name == child.parent)
.select(
child.required_qty,
child.consumed_qty,
(child.billed_qty - child.returned_qty).as_("qty"),
child.rm_item_code,
child.stock_uom,
child.name,
)
.where(
(parent.docstatus == 1)
& (parent.sales_order == source_name)
& (child.is_customer_provided_item == 0)
)
)
result = query.run(as_dict=True)
if result:
idx = len(doclist.items) + 1
for item in result:
if (qty := max(item.required_qty, item.consumed_qty) - item.qty) > 0:
doclist.append(
"items",
{
"item_code": item.rm_item_code,
"qty": qty,
"uom": item.stock_uom,
"scio_detail": item.name,
},
)
doclist.process_item_selection(idx)
idx += 1
doclist.has_subcontracted = 1
doclist = get_mapped_doc(
"Sales Order",
source_name,
@@ -1328,6 +1429,9 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
ignore_permissions=ignore_permissions,
)
if frappe.get_cached_value("Sales Order", source_name, "is_subcontracted"):
add_self_rm(doclist)
automatically_fetch_payment_terms = cint(
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
)
@@ -2005,3 +2109,71 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
@frappe.whitelist()
def get_stock_reservation_status():
return frappe.get_single_value("Stock Settings", "enable_stock_reservation")
@frappe.whitelist()
def make_subcontracting_inward_order(source_name, target_doc=None):
if not is_so_fully_subcontracted(source_name):
return get_mapped_subcontracting_inward_order(source_name, target_doc)
else:
frappe.throw(_("This Sales Order has been fully subcontracted."))
def is_so_fully_subcontracted(so_name):
table = frappe.qb.DocType("Sales Order Item")
query = (
frappe.qb.from_(table)
.select(table.name)
.where((table.parent == so_name) & (table.qty != table.subcontracted_qty))
)
return not query.run(as_dict=True)
def get_mapped_subcontracting_inward_order(source_name, target_doc=None):
def post_process(source_doc, target_doc):
if (
frappe.db.count(
"Warehouse", {"customer": source_doc.customer, "disabled": 0, "is_rejected_warehouse": 0}
)
== 1
):
target_doc.customer_warehouse = frappe.get_cached_value(
"Warehouse",
{"customer": source_doc.customer, "disabled": 0, "is_rejected_warehouse": 0},
"name",
)
target_doc.populate_items_table()
if target_doc and isinstance(target_doc, str):
target_doc = json.loads(target_doc)
for key in ["service_items", "items", "received_items"]:
if key in target_doc:
del target_doc[key]
target_doc = json.dumps(target_doc)
target_doc = get_mapped_doc(
"Sales Order",
source_name,
{
"Sales Order": {
"doctype": "Subcontracting Inward Order",
"field_map": {},
"field_no_map": ["total_qty", "total", "net_total"],
"validation": {
"docstatus": ["=", 1],
},
},
"Sales Order Item": {
"doctype": "Subcontracting Inward Order Service Item",
"field_map": {
"name": "sales_order_item",
},
"field_no_map": ["qty", "fg_item_qty", "amount"],
"condition": lambda item: item.qty != item.subcontracted_qty,
},
},
target_doc,
post_process,
)
return target_doc

View File

@@ -30,5 +30,6 @@ def get_data():
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]},
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
{"label": _("Schedule"), "items": ["Delivery Schedule Item"]},
{"label": _("Subcontracting Inward"), "items": ["Subcontracting Inward Order"]},
],
}

View File

@@ -2613,6 +2613,7 @@ def make_sales_order(**args):
so.customer = args.customer or "_Test Customer"
so.currency = args.currency or "INR"
so.po_no = args.po_no or ""
so.is_subcontracted = args.is_subcontracted or 0
if args.selling_price_list:
so.selling_price_list = args.selling_price_list

View File

@@ -7,6 +7,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"fg_item",
"fg_item_qty",
"item_code",
"customer_item_code",
"ensure_delivery_based_on_produced_serial_no",
@@ -25,6 +27,7 @@
"quantity_and_rate",
"qty",
"stock_uom",
"subcontracted_qty",
"col_break2",
"uom",
"conversion_factor",
@@ -468,6 +471,7 @@
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.delivered_by_supplier==1||doc.supplier",
"depends_on": "eval:!parent.is_subcontracted",
"fieldname": "drop_ship_section",
"fieldtype": "Section Break",
"label": "Drop Ship",
@@ -490,6 +494,7 @@
},
{
"collapsible": 1,
"depends_on": "eval:!parent.is_subcontracted",
"fieldname": "item_weight_details",
"fieldtype": "Section Break",
"label": "Item Weight Details"
@@ -517,6 +522,7 @@
"options": "UOM"
},
{
"depends_on": "eval:!parent.is_subcontracted",
"fieldname": "warehouse_and_reference",
"fieldtype": "Section Break",
"label": "Warehouse and Reference"
@@ -879,7 +885,7 @@
{
"allow_on_submit": 1,
"default": "1",
"depends_on": "eval:doc.is_stock_item",
"depends_on": "eval:(doc.is_stock_item && !parent.is_subcontracted)",
"fieldname": "reserve_stock",
"fieldtype": "Check",
"label": "Reserve Stock",
@@ -935,6 +941,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!parent.is_subcontracted",
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
@@ -977,12 +984,39 @@
"fieldname": "add_schedule",
"fieldtype": "Button",
"label": "Add Schedule"
},
{
"allow_on_submit": 1,
"default": "0",
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "subcontracted_qty",
"fieldtype": "Float",
"label": "Subcontracted Quantity",
"no_copy": 1,
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "fg_item",
"fieldtype": "Link",
"label": "Finished Good",
"mandatory_depends_on": "eval:parent.is_subcontracted",
"options": "Item"
},
{
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "fg_item_qty",
"fieldtype": "Float",
"label": "Finished Good Qty",
"mandatory_depends_on": "eval:parent.is_subcontracted"
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-08-21 17:01:54.269105",
"modified": "2025-10-13 10:57:43.378448",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -42,6 +42,8 @@ class SalesOrderItem(Document):
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
ensure_delivery_based_on_produced_serial_no: DF.Check
fg_item: DF.Link | None
fg_item_qty: DF.Float
grant_commission: DF.Check
gross_profit: DF.Currency
image: DF.Attach | None
@@ -84,6 +86,7 @@ class SalesOrderItem(Document):
stock_reserved_qty: DF.Float
stock_uom: DF.Link | None
stock_uom_rate: DF.Currency
subcontracted_qty: DF.Float
supplier: DF.Link | None
target_warehouse: DF.Link | None
total_weight: DF.Float

View File

@@ -11,6 +11,7 @@
"customer_group",
"column_break_4",
"territory",
"item_price_tab",
"item_price_settings_section",
"selling_price_list",
"maintain_same_rate_action",
@@ -22,6 +23,7 @@
"validate_selling_price",
"editable_bundle_item_rates",
"allow_negative_rates_for_items",
"transaction_tab",
"sales_transactions_settings_section",
"so_required",
"dn_required",
@@ -38,7 +40,12 @@
"allow_zero_qty_in_quotation",
"allow_zero_qty_in_sales_order",
"experimental_section",
"use_legacy_js_reactivity"
"use_legacy_js_reactivity",
"subcontracting_inward_tab",
"section_break_zwh6",
"allow_delivery_of_overproduced_qty",
"column_break_mla9",
"deliver_scrap_items"
],
"fields": [
{
@@ -232,6 +239,44 @@
"fieldtype": "Check",
"label": "Allow Quotation with Zero Quantity"
},
{
"fieldname": "section_break_zwh6",
"fieldtype": "Section Break",
"label": "Subcontracting Inward Settings"
},
{
"default": "0",
"description": "If enabled, system will allow user to deliver the entire quantity of the finished goods produced against the Subcontracting Inward Order. If disabled, system will allow delivery of only the ordered quantity.",
"fieldname": "allow_delivery_of_overproduced_qty",
"fieldtype": "Check",
"label": "Allow Delivery of Overproduced Qty"
},
{
"fieldname": "column_break_mla9",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, the Scrap Item generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good.",
"fieldname": "deliver_scrap_items",
"fieldtype": "Check",
"label": "Deliver Scrap Items"
},
{
"fieldname": "item_price_tab",
"fieldtype": "Tab Break",
"label": "Item Price"
},
{
"fieldname": "transaction_tab",
"fieldtype": "Tab Break",
"label": "Transaction"
},
{
"fieldname": "subcontracting_inward_tab",
"fieldtype": "Tab Break",
"label": "Subcontracting Inward"
},
{
"default": "0",
"fieldname": "fallback_to_default_price_list",
@@ -251,7 +296,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-09-24 16:08:48.865885",
"modified": "2025-10-12 16:08:48.865885",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -21,6 +21,7 @@ class SellingSettings(Document):
from frappe.types import DF
allow_against_multiple_purchase_orders: DF.Check
allow_delivery_of_overproduced_qty: DF.Check
allow_multiple_items: DF.Check
allow_negative_rates_for_items: DF.Check
allow_sales_order_creation_for_expired_quotation: DF.Check
@@ -29,6 +30,7 @@ class SellingSettings(Document):
blanket_order_allowance: DF.Float
cust_master_name: DF.Literal["Customer Name", "Naming Series", "Auto Name"]
customer_group: DF.Link | None
deliver_scrap_items: DF.Check
dn_required: DF.Literal["No", "Yes"]
dont_reserve_sales_order_qty_on_sales_return: DF.Check
editable_bundle_item_rates: DF.Check

View File

@@ -289,6 +289,7 @@ erpnext.company.setup_queries = function (frm) {
["default_provisional_account", { root_type: ["in", ["Liability", "Asset"]] }],
["default_advance_received_account", { root_type: "Liability", account_type: "Receivable" }],
["default_advance_paid_account", { root_type: "Asset", account_type: "Payable" }],
["service_expense_account", { root_type: "Expense" }],
],
function (i, v) {
erpnext.company.set_custom_query(frm, v);

View File

@@ -108,6 +108,7 @@
"transactions_annual_history",
"purchase_expense_section",
"purchase_expense_account",
"service_expense_account",
"column_break_ereg",
"purchase_expense_contra_account",
"stock_tab",
@@ -869,6 +870,13 @@
"fieldtype": "Link",
"label": "Purchase Expense Contra Account",
"options": "Account"
},
{
"description": "For service item",
"fieldname": "service_expense_account",
"fieldtype": "Link",
"label": "Service Expense Account",
"options": "Account"
}
],
"icon": "fa fa-building",
@@ -876,7 +884,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-10-01 17:34:10.971627",
"modified": "2025-10-10 15:12:37.941251",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -100,6 +100,7 @@ class Company(NestedSet):
round_off_for_opening: DF.Link | None
sales_monthly_history: DF.SmallText | None
series_for_depreciation_entry: DF.Data | None
service_expense_account: DF.Link | None
stock_adjustment_account: DF.Link | None
stock_received_but_not_billed: DF.Link | None
submit_err_jv: DF.Check
@@ -570,6 +571,21 @@ class Company(NestedSet):
self.db_set("disposal_account", disposal_acct)
if not self.service_expense_account:
service_expense_acct = frappe.db.get_value(
"Account",
{
"account_name": _("Marketing Expenses"),
"company": self.name,
"is_group": 0,
"root_type": "Expense",
},
"name",
)
if service_expense_acct:
self.db_set("service_expense_account", service_expense_acct)
def _set_default_account(self, fieldname, account_type):
if self.get(fieldname):
return

View File

@@ -44,7 +44,7 @@ class SalesPartner(WebsiteGenerator):
load_address_and_contact(self)
def autoname(self):
self.name = self.partner_name
pass
def validate(self):
if not self.route:

View File

@@ -9,6 +9,7 @@ from frappe.desk.notifications import clear_notifications
from frappe.model.document import Document
from frappe.utils import cint, comma_and, create_batch, get_link_to_form
from frappe.utils.background_jobs import get_job, is_job_enqueued
from frappe.utils.caching import request_cache
LEDGER_ENTRY_DOCTYPES = frozenset(
(
@@ -482,6 +483,7 @@ def get_doctypes_to_be_ignored():
@frappe.whitelist()
@request_cache
def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None):
if not company:
return

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
import os
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@@ -34,6 +36,7 @@ def after_install():
update_roles()
make_default_operations()
update_pegged_currencies()
create_letter_head()
frappe.db.commit()
@@ -250,6 +253,20 @@ def update_roles():
def create_default_role_profiles():
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
if frappe.db.exists("Role Profile", role_profile_name):
role_profile = frappe.get_doc("Role Profile", role_profile_name)
existing_roles = [row.role for row in role_profile.roles]
role_profile.roles = [row for row in role_profile.roles if row.role in roles]
for role in roles:
if role not in existing_roles:
role_profile.append("roles", {"role": role})
role_profile.save(ignore_permissions=True)
continue
role_profile = frappe.new_doc("Role Profile")
role_profile.role_profile = role_profile_name
for role in roles:
@@ -279,6 +296,28 @@ def update_pegged_currencies():
doc.save()
def create_letter_head():
base_path = frappe.get_app_path("erpnext", "accounts", "letterhead")
letterheads = {
"Company Letterhead": "company_letterhead.html",
"Company Letterhead - Grey": "company_letterhead_grey.html",
}
for name, filename in letterheads.items():
if not frappe.db.exists("Letter Head", name):
content = frappe.read_file(os.path.join(base_path, filename))
doc = frappe.get_doc(
{
"doctype": "Letter Head",
"letter_head_name": name,
"source": "HTML",
"content": content,
}
)
doc.insert(ignore_permissions=True)
DEFAULT_ROLE_PROFILES = {
"Inventory": [
"Stock User",

View File

@@ -122,6 +122,30 @@ def install(country=None):
"purpose": "Material Consumption for Manufacture",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": _("Receive from Customer"),
"purpose": "Receive from Customer",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": _("Return Raw Material to Customer"),
"purpose": "Return Raw Material to Customer",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": _("Subcontracting Delivery"),
"purpose": "Subcontracting Delivery",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": _("Subcontracting Return"),
"purpose": "Subcontracting Return",
"is_standard": 1,
},
# territory: with two default territories, one for home country and one named Rest of the World
{
"doctype": "Territory",

View File

@@ -81,6 +81,8 @@ def update_page_info(bootinfo):
def bootinfo(bootinfo):
if bootinfo.get("user") and bootinfo["user"].get("name"):
bootinfo["user"]["employee"] = ""
frappe.session.data.employee = ""
employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name")
if employee:
bootinfo["user"]["employee"] = employee
frappe.session.data.employee = employee

View File

@@ -158,7 +158,9 @@ class Batch(Document):
@frappe.whitelist()
def recalculate_batch_qty(self):
batches = get_batch_qty(batch_no=self.name, item_code=self.item)
batches = get_batch_qty(
batch_no=self.name, item_code=self.item, for_stock_levels=True, consider_negative_batches=True
)
batch_qty = 0.0
if batches:
for row in batches:
@@ -260,6 +262,7 @@ def get_batch_qty(
"warehouse": warehouse,
"creation": creation,
"batch_no": batch_no,
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
"ignore_voucher_nos": ignore_voucher_nos,
"for_stock_levels": for_stock_levels,
"consider_negative_batches": consider_negative_batches,

View File

@@ -334,6 +334,7 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
if (
doc.docstatus == 1 &&
!doc.is_return &&
doc.per_returned != 100 &&
doc.status != "Closed" &&
flt(doc.per_billed) < 100 &&
frappe.model.can_create("Sales Invoice")

View File

@@ -10,6 +10,8 @@ from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder import DocType
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import cint, flt
from erpnext.accounts.party import get_due_date
@@ -792,35 +794,39 @@ def get_list_context(context=None):
def get_invoiced_qty_map(delivery_note):
"""returns a map: {dn_detail: invoiced_qty}"""
invoiced_qty_map = {}
sii = DocType("Sales Invoice Item")
for dn_detail, qty in frappe.db.sql(
"""select dn_detail, qty from `tabSales Invoice Item`
where delivery_note=%s and docstatus=1""",
delivery_note,
):
if not invoiced_qty_map.get(dn_detail):
invoiced_qty_map[dn_detail] = 0
invoiced_qty_map[dn_detail] += qty
invoiced_qty_map = frappe._dict(
(
frappe.qb.from_(sii)
.select(sii.dn_detail, Sum(sii.qty).as_("qty"))
.where((sii.delivery_note == delivery_note) & (sii.docstatus == 1))
.groupby(sii.dn_detail)
).run()
)
return invoiced_qty_map
def get_returned_qty_map(delivery_note):
"""returns a map: {so_detail: returned_qty}"""
dn = DocType("Delivery Note")
dni = DocType("Delivery Note Item")
returned_qty_map = frappe._dict(
frappe.db.sql(
"""select dn_item.dn_detail, sum(abs(dn_item.qty)) as qty
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
where dn.name = dn_item.parent
and dn.docstatus = 1
and dn.is_return = 1
and dn.return_against = %s
and dn_item.qty <= 0
group by dn_item.item_code
""",
delivery_note,
)
(
frappe.qb.from_(dni)
.join(dn)
.on(dn.name == dni.parent)
.select(dni.dn_detail, Sum(Abs(dni.qty)).as_("qty"))
.where(
(dn.docstatus == 1)
& (dn.is_return == 1)
& (dn.return_against == delivery_note)
& (dni.qty <= 0)
)
.groupby(dni.dn_detail)
).run()
)
return returned_qty_map

View File

@@ -2659,6 +2659,127 @@ class TestDeliveryNote(IntegrationTestCase):
status = frappe.db.get_value("Serial No", row, "status")
self.assertEqual(status, "Active")
def test_sales_return_for_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
from erpnext.stock.doctype.item.test_item import make_item
rm_items = []
for item_code, properties in {
"_Packed Service Item": {"is_stock_item": 0},
"_Packed FG Item New 1": {
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "SN-PACKED-1-.#####",
},
"_Packed FG Item New 2": {
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-PACKED-2-.#####",
},
"_Packed FG Item New 3": {
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "BATCH-PACKED-3-.#####",
"has_serial_no": 1,
"serial_no_series": "SN-PACKED-3-.#####",
},
}.items():
if not frappe.db.exists("Item", item_code):
make_item(item_code, properties)
if item_code != "_Packed Service Item":
rm_items.append(item_code)
for rate in [100, 200]:
make_stock_entry(item=item_code, target="_Test Warehouse - _TC", qty=5, rate=rate)
make_product_bundle("_Packed Service Item", rm_items)
dn = create_delivery_note(
item_code="_Packed Service Item",
warehouse="_Test Warehouse - _TC",
qty=5,
)
serial_batch_map = {}
for row in dn.packed_items:
self.assertTrue(row.serial_and_batch_bundle)
if row.item_code not in serial_batch_map:
serial_batch_map[row.item_code] = frappe._dict(
{
"serial_nos": [],
"batches": defaultdict(int),
"serial_no_valuation": defaultdict(float),
"batch_no_valuation": defaultdict(float),
}
)
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for entry in doc.entries:
if entry.serial_no:
serial_batch_map[row.item_code].serial_nos.append(entry.serial_no)
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no] = entry.incoming_rate
if entry.batch_no:
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no] = entry.incoming_rate
dn1 = make_sales_return(dn.name)
dn1.items[0].qty = -2
dn1.submit()
dn1.reload()
for row in dn1.packed_items:
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for entry in doc.entries:
if entry.serial_no:
self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no],
)
serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no)
serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no)
elif entry.batch_no:
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches)
self.assertEqual(entry.qty, 2.0)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no],
)
dn2 = make_sales_return(dn.name)
dn2.items[0].qty = -3
dn2.submit()
dn2.reload()
for row in dn2.packed_items:
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
for entry in doc.entries:
if entry.serial_no:
self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no],
)
serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no)
serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no)
elif entry.batch_no:
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
self.assertEqual(serial_batch_map[row.item_code].batches[entry.batch_no], 0.0)
self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches)
self.assertEqual(entry.qty, 3.0)
self.assertEqual(
entry.incoming_rate,
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no],
)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -221,7 +221,13 @@ frappe.ui.form.on("Item", {
const stock_exists = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0;
["is_stock_item", "has_serial_no", "has_batch_no", "has_variants"].forEach((fieldname) => {
[
"is_stock_item",
"is_customer_provided_item",
"has_serial_no",
"has_batch_no",
"has_variants",
].forEach((fieldname) => {
frm.set_df_property(fieldname, "read_only", stock_exists);
});

View File

@@ -586,7 +586,7 @@
"label": "Is Customer Provided Item"
},
{
"depends_on": "eval:doc.is_customer_provided_item==1",
"depends_on": "eval:doc.is_customer_provided_item",
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
@@ -771,10 +771,9 @@
},
{
"default": "0",
"description": "If subcontracted to a vendor",
"fieldname": "is_sub_contracted_item",
"fieldtype": "Check",
"label": "Supply Raw Materials for Purchase",
"label": "Is Subcontracted Item",
"oldfieldname": "is_sub_contracted_item",
"oldfieldtype": "Select"
},
@@ -954,7 +953,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2025-10-01 16:58:40.946604",
"modified": "2025-10-13 16:58:40.946604",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -965,7 +965,12 @@ class Item(Document):
if self.is_new():
return
restricted_fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
restricted_fields = (
"has_serial_no",
"is_stock_item",
"valuation_method",
"has_batch_no",
)
values = frappe.db.get_value("Item", self.name, restricted_fields, as_dict=True)
if not values:

View File

@@ -67,6 +67,10 @@ class PackedItem(Document):
def make_packing_list(doc):
"Make/Update packing list for Product Bundle Item."
if doc.get("is_subcontracted"):
return
if doc.get("_action") and doc._action == "update_after_submit":
return

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