mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-05 06:28:29 +00:00
Merge branch 'develop' into tds-jv
This commit is contained in:
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -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.
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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
|
||||
|
||||
-->
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
108
erpnext/accounts/letterhead/company_letterhead.html
Normal file
108
erpnext/accounts/letterhead/company_letterhead.html
Normal 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>
|
||||
125
erpnext/accounts/letterhead/company_letterhead_grey.html
Normal file
125
erpnext/accounts/letterhead/company_letterhead_grey.html
Normal 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
@@ -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 += [
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -284,15 +284,15 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_message():
|
||||
return """<span class="indicator">
|
||||
Valid till :
|
||||
return f"""<span class="indicator">
|
||||
{_("Valid Till")}:
|
||||
</span>
|
||||
<span class="indicator orange">
|
||||
Expires in a week or less
|
||||
{_("Expires in a week or less")}
|
||||
</span>
|
||||
|
||||
<span class="indicator red">
|
||||
Expires today / Already Expired
|
||||
{_("Expires today or already expired")}
|
||||
</span>"""
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
1021
erpnext/controllers/subcontracting_inward_controller.py
Normal file
1021
erpnext/controllers/subcontracting_inward_controller.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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):
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
63249
erpnext/locale/my.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-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:"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}%"))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
14
erpnext/patches/v16_0/add_new_stock_entry_types.py
Normal file
14
erpnext/patches/v16_0/add_new_stock_entry_types.py
Normal 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
|
||||
)
|
||||
7
erpnext/patches/v16_0/rename_subcontracted_quantity.py
Normal file
7
erpnext/patches/v16_0/rename_subcontracted_quantity.py
Normal 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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
145
erpnext/public/js/print.js
Normal 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;
|
||||
}
|
||||
});
|
||||
@@ -195,6 +195,7 @@ $.extend(erpnext.stock_reservation, {
|
||||
args: {
|
||||
doc: frm.doc,
|
||||
items: data.items,
|
||||
is_transfer: 0,
|
||||
table_name: table_name,
|
||||
notify: true,
|
||||
},
|
||||
|
||||
@@ -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}}">
|
||||
|
||||
@@ -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(
|
||||
__(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user