mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-13 10:11:20 +00:00
Merge pull request #52348 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -60,7 +60,7 @@ body:
|
|||||||
description: Share exact version number of Frappe and ERPNext you are using.
|
description: Share exact version number of Frappe and ERPNext you are using.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Frappe version -
|
Frappe version -
|
||||||
ERPNext Verion -
|
ERPNext version -
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,17 @@
|
|||||||
},
|
},
|
||||||
"account_number": "1151.000"
|
"account_number": "1151.000"
|
||||||
},
|
},
|
||||||
|
"Pajak Dibayar di Muka": {
|
||||||
|
"PPN Masukan": {
|
||||||
|
"account_number": "1152.001",
|
||||||
|
"account_type": "Tax"
|
||||||
|
},
|
||||||
|
"PPh 23 Dibayar di Muka": {
|
||||||
|
"account_number": "1152.002",
|
||||||
|
"account_type": "Tax"
|
||||||
|
},
|
||||||
|
"account_number": "1152.000"
|
||||||
|
},
|
||||||
"account_number": "1150.000"
|
"account_number": "1150.000"
|
||||||
},
|
},
|
||||||
"Kas": {
|
"Kas": {
|
||||||
|
|||||||
@@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
is_company_account: function (frm) {
|
|
||||||
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Company Account",
|
"label": "Company Account",
|
||||||
|
"mandatory_depends_on": "is_company_account",
|
||||||
"options": "Account"
|
"options": "Account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -98,6 +99,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
|
"mandatory_depends_on": "is_company_account",
|
||||||
"options": "Company"
|
"options": "Company"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -252,7 +254,7 @@
|
|||||||
"link_fieldname": "default_bank_account"
|
"link_fieldname": "default_bank_account"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-08-29 12:32:01.081687",
|
"modified": "2026-01-20 00:46:16.633364",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Account",
|
"name": "Bank Account",
|
||||||
|
|||||||
@@ -52,31 +52,35 @@ class BankAccount(Document):
|
|||||||
delete_contact_and_address("Bank Account", self.name)
|
delete_contact_and_address("Bank Account", self.name)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_company()
|
self.validate_is_company_account()
|
||||||
self.validate_account()
|
|
||||||
self.update_default_bank_account()
|
self.update_default_bank_account()
|
||||||
|
|
||||||
def validate_account(self):
|
def validate_is_company_account(self):
|
||||||
if self.account:
|
if self.is_company_account:
|
||||||
if accounts := frappe.db.get_all(
|
if not self.company:
|
||||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
frappe.throw(_("Company is mandatory for company account"))
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
|
||||||
frappe.bold(self.account),
|
|
||||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_company(self):
|
if not self.account:
|
||||||
if self.is_company_account and not self.company:
|
frappe.throw(_("Company Account is mandatory"))
|
||||||
frappe.throw(_("Company is manadatory for company account"))
|
|
||||||
|
self.validate_account()
|
||||||
|
|
||||||
@deprecated
|
@deprecated
|
||||||
def validate_iban(self):
|
def validate_iban(self):
|
||||||
"""Kept for backward compatibility, will be removed in v16."""
|
"""Kept for backward compatibility, will be removed in v16."""
|
||||||
validate_iban(self.iban, throw=True)
|
validate_iban(self.iban, throw=True)
|
||||||
|
|
||||||
|
def validate_account(self):
|
||||||
|
if accounts := frappe.db.get_all(
|
||||||
|
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||||
|
frappe.bold(self.account),
|
||||||
|
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def update_default_bank_account(self):
|
def update_default_bank_account(self):
|
||||||
if self.is_default and not self.disabled:
|
if self.is_default and not self.disabled:
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
|
|||||||
let d = locals[cdt][cdn];
|
let d = locals[cdt][cdn];
|
||||||
return {
|
return {
|
||||||
filters: [
|
filters: [
|
||||||
["Account", "account_type", "in", "Bank, Cash, Receivable"],
|
["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
|
||||||
["Account", "is_group", "=", 0],
|
["Account", "is_group", "=", 0],
|
||||||
["Account", "company", "=", d.company],
|
["Account", "company", "=", d.company],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -838,6 +838,53 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||||
self.assertEqual(batch.qty, 5)
|
self.assertEqual(batch.qty, 5)
|
||||||
|
|
||||||
|
def test_pos_batch_reservation_with_return_qty(self):
|
||||||
|
"""
|
||||||
|
Test POS Invoice reserved qty for batch without bundle with return invoices.
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
|
get_auto_batch_nos,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
|
create_batch_item_with_batch,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
|
||||||
|
se = make_stock_entry(
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
item_code="_Batch Item Reserve Return",
|
||||||
|
qty=30,
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
se.reload()
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
# POS Invoice for the batch without bundle
|
||||||
|
pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
|
||||||
|
pos_inv.append(
|
||||||
|
"payments",
|
||||||
|
{"mode_of_payment": "Cash", "amount": 4500},
|
||||||
|
)
|
||||||
|
pos_inv.items[0].batch_no = batch_no
|
||||||
|
pos_inv.save()
|
||||||
|
pos_inv.submit()
|
||||||
|
|
||||||
|
# POS Invoice return
|
||||||
|
pos_return = make_sales_return(pos_inv.name)
|
||||||
|
|
||||||
|
pos_return.insert()
|
||||||
|
pos_return.submit()
|
||||||
|
|
||||||
|
batches = get_auto_batch_nos(
|
||||||
|
frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
|
||||||
|
)
|
||||||
|
|
||||||
|
for batch in batches:
|
||||||
|
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||||
|
self.assertEqual(batch.qty, 30)
|
||||||
|
|
||||||
def test_pos_batch_item_qty_validation(self):
|
def test_pos_batch_item_qty_validation(self):
|
||||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
BatchNegativeStockError,
|
BatchNegativeStockError,
|
||||||
|
|||||||
@@ -697,6 +697,7 @@ def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
|
|||||||
& (SalesInvoice.is_return == 0)
|
& (SalesInvoice.is_return == 0)
|
||||||
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
|
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
|
||||||
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
|
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
|
||||||
|
& (SalesInvoice.docstatus == 1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -412,8 +412,9 @@ def reconcile(doc: None | str = None) -> None:
|
|||||||
for x in allocations:
|
for x in allocations:
|
||||||
pr.append("allocation", x)
|
pr.append("allocation", x)
|
||||||
|
|
||||||
|
skip_ref_details_update_for_pe = check_multi_currency(pr)
|
||||||
# reconcile
|
# reconcile
|
||||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
|
pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
|
||||||
|
|
||||||
# If Payment Entry, update details only for newly linked references
|
# If Payment Entry, update details only for newly linked references
|
||||||
# This is for performance
|
# This is for performance
|
||||||
@@ -503,6 +504,37 @@ def reconcile(doc: None | str = None) -> None:
|
|||||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||||
|
|
||||||
|
|
||||||
|
def check_multi_currency(pr_doc):
|
||||||
|
GL = frappe.qb.DocType("GL Entry")
|
||||||
|
Account = frappe.qb.DocType("Account")
|
||||||
|
|
||||||
|
def get_account_currency(voucher_type, voucher_no):
|
||||||
|
currency = (
|
||||||
|
frappe.qb.from_(GL)
|
||||||
|
.join(Account)
|
||||||
|
.on(GL.account == Account.name)
|
||||||
|
.select(Account.account_currency)
|
||||||
|
.where(
|
||||||
|
(GL.voucher_type == voucher_type)
|
||||||
|
& (GL.voucher_no == voucher_no)
|
||||||
|
& (Account.account_type.isin(["Payable", "Receivable"]))
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
return currency[0].account_currency if currency else None
|
||||||
|
|
||||||
|
for allocation in pr_doc.allocation:
|
||||||
|
reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
|
||||||
|
|
||||||
|
invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
|
||||||
|
|
||||||
|
if reference_currency != invoice_currency:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||||
running_doc = None
|
running_doc = None
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
"Bank Transaction",
|
"Bank Transaction",
|
||||||
|
"Packing Slip",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-6">
|
<div class="col-xs-6">
|
||||||
<table>
|
<table>
|
||||||
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
|
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.posting_date) }}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -159,11 +159,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
|||||||
|
|
||||||
|
|
||||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||||
labels = [d.get("label") for d in columns[2:]]
|
labels = [d.get("label") for d in columns[4:]]
|
||||||
|
|
||||||
income_data, expense_data, net_profit = [], [], []
|
income_data, expense_data, net_profit = [], [], []
|
||||||
|
|
||||||
for p in columns[2:]:
|
for p in columns[4:]:
|
||||||
if income:
|
if income:
|
||||||
income_data.append(income[-2].get(p.get("fieldname")))
|
income_data.append(income[-2].get(p.get("fieldname")))
|
||||||
if expense:
|
if expense:
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ frappe.query_reports["Trial Balance"] = {
|
|||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "show_group_accounts",
|
||||||
|
label: __("Show Group Accounts"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
formatter: erpnext.financial_statements.formatter,
|
formatter: erpnext.financial_statements.formatter,
|
||||||
tree: true,
|
tree: true,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def validate_filters(filters):
|
|||||||
|
|
||||||
def get_data(filters):
|
def get_data(filters):
|
||||||
accounts = frappe.db.sql(
|
accounts = frappe.db.sql(
|
||||||
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
|
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
|
||||||
|
|
||||||
from `tabAccount` where company=%s order by lft""",
|
from `tabAccount` where company=%s order by lft""",
|
||||||
filters.company,
|
filters.company,
|
||||||
@@ -393,6 +393,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
|||||||
"from_date": filters.from_date,
|
"from_date": filters.from_date,
|
||||||
"to_date": filters.to_date,
|
"to_date": filters.to_date,
|
||||||
"currency": company_currency,
|
"currency": company_currency,
|
||||||
|
"is_group_account": d.is_group,
|
||||||
"account_name": (
|
"account_name": (
|
||||||
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
|
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
|
||||||
),
|
),
|
||||||
@@ -409,6 +410,10 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
|||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
total_row = calculate_total_row(accounts, company_currency)
|
total_row = calculate_total_row(accounts, company_currency)
|
||||||
|
|
||||||
|
if not filters.get("show_group_accounts"):
|
||||||
|
data = hide_group_accounts(data)
|
||||||
|
|
||||||
data.extend([{}, total_row])
|
data.extend([{}, total_row])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -488,3 +493,12 @@ def prepare_opening_closing(row):
|
|||||||
row[valid_col] = 0.0
|
row[valid_col] = 0.0
|
||||||
else:
|
else:
|
||||||
row[reverse_col] = 0.0
|
row[reverse_col] = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def hide_group_accounts(data):
|
||||||
|
non_group_accounts_data = []
|
||||||
|
for d in data:
|
||||||
|
if not d.get("is_group_account"):
|
||||||
|
d.update(indent=0)
|
||||||
|
non_group_accounts_data.append(d)
|
||||||
|
return non_group_accounts_data
|
||||||
|
|||||||
@@ -304,12 +304,17 @@ class RequestforQuotation(BuyingController):
|
|||||||
else:
|
else:
|
||||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||||
|
|
||||||
|
rendered_message = frappe.render_template(self.message_for_supplier, doc_args)
|
||||||
|
subject_source = (
|
||||||
|
self.subject
|
||||||
|
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||||
|
or _("Request for Quotation")
|
||||||
|
)
|
||||||
|
rendered_subject = frappe.render_template(subject_source, doc_args)
|
||||||
if preview:
|
if preview:
|
||||||
return {
|
return {
|
||||||
"message": self.message_for_supplier,
|
"message": rendered_message,
|
||||||
"subject": self.subject
|
"subject": rendered_subject,
|
||||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
|
||||||
or _("Request for Quotation"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attachments = []
|
attachments = []
|
||||||
@@ -333,10 +338,8 @@ class RequestforQuotation(BuyingController):
|
|||||||
self.send_email(
|
self.send_email(
|
||||||
data,
|
data,
|
||||||
sender,
|
sender,
|
||||||
self.subject
|
rendered_subject,
|
||||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
rendered_message,
|
||||||
or _("Request for Quotation"),
|
|
||||||
self.message_for_supplier,
|
|
||||||
attachments,
|
attachments,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -770,12 +770,34 @@ def get_filters(
|
|||||||
if reference_voucher_detail_no:
|
if reference_voucher_detail_no:
|
||||||
filters["voucher_detail_no"] = reference_voucher_detail_no
|
filters["voucher_detail_no"] = reference_voucher_detail_no
|
||||||
|
|
||||||
if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row and item_row.get("warehouse"):
|
warehouses = []
|
||||||
filters["warehouse"] = item_row.get("warehouse")
|
if voucher_type in ["Purchase Receipt", "Purchase Invoice"] and item_row:
|
||||||
|
if reference_voucher_detail_no:
|
||||||
|
warehouses = get_warehouses_for_return(voucher_type, reference_voucher_detail_no)
|
||||||
|
|
||||||
|
if item_row.get("warehouse") and item_row.get("warehouse") in warehouses:
|
||||||
|
filters["warehouse"] = item_row.get("warehouse")
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
|
def get_warehouses_for_return(voucher_type, name):
|
||||||
|
warehouses = []
|
||||||
|
warehouse_details = frappe.get_all(
|
||||||
|
voucher_type + " Item",
|
||||||
|
filters={"name": name, "docstatus": 1},
|
||||||
|
fields=["warehouse", "rejected_warehouse"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in warehouse_details:
|
||||||
|
if d.warehouse:
|
||||||
|
warehouses.append(d.warehouse)
|
||||||
|
if d.rejected_warehouse:
|
||||||
|
warehouses.append(d.rejected_warehouse)
|
||||||
|
|
||||||
|
return warehouses
|
||||||
|
|
||||||
|
|
||||||
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None):
|
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_voucher_detail_no=None):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import (
|
from erpnext.stock.doctype.serial_no.serial_no import (
|
||||||
get_serial_nos as get_serial_nos_from_serial_no,
|
get_serial_nos as get_serial_nos_from_serial_no,
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ class SellingController(StockController):
|
|||||||
_(
|
_(
|
||||||
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
|
"""Row #{0}: Selling rate for item {1} is lower than its {2}.
|
||||||
Selling {3} should be atleast {4}.<br><br>Alternatively,
|
Selling {3} should be atleast {4}.<br><br>Alternatively,
|
||||||
you can disable selling price validation in {5} to bypass
|
you can disable '{5}' in {6} to bypass
|
||||||
this validation."""
|
this validation."""
|
||||||
).format(
|
).format(
|
||||||
idx,
|
idx,
|
||||||
@@ -287,6 +287,7 @@ class SellingController(StockController):
|
|||||||
bold(ref_rate_field),
|
bold(ref_rate_field),
|
||||||
bold("net rate"),
|
bold("net rate"),
|
||||||
bold(rate),
|
bold(rate),
|
||||||
|
bold(frappe.get_meta("Selling Settings").get_label("validate_selling_price")),
|
||||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||||
),
|
),
|
||||||
title=_("Invalid Selling Price"),
|
title=_("Invalid Selling Price"),
|
||||||
@@ -298,7 +299,6 @@ class SellingController(StockController):
|
|||||||
return
|
return
|
||||||
|
|
||||||
is_internal_customer = self.get("is_internal_customer")
|
is_internal_customer = self.get("is_internal_customer")
|
||||||
valuation_rate_map = {}
|
|
||||||
|
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
if not item.item_code or item.is_free_item:
|
if not item.item_code or item.is_free_item:
|
||||||
@@ -308,7 +308,9 @@ class SellingController(StockController):
|
|||||||
"Item", item.item_code, ("last_purchase_rate", "is_stock_item")
|
"Item", item.item_code, ("last_purchase_rate", "is_stock_item")
|
||||||
)
|
)
|
||||||
|
|
||||||
last_purchase_rate_in_sales_uom = last_purchase_rate * (item.conversion_factor or 1)
|
last_purchase_rate_in_sales_uom = flt(
|
||||||
|
last_purchase_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||||
|
)
|
||||||
|
|
||||||
if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
|
if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
|
||||||
throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
|
throw_message(item.idx, item.item_name, last_purchase_rate_in_sales_uom, "last purchase rate")
|
||||||
@@ -316,50 +318,16 @@ class SellingController(StockController):
|
|||||||
if is_internal_customer or not is_stock_item:
|
if is_internal_customer or not is_stock_item:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
valuation_rate_map[(item.item_code, item.warehouse)] = None
|
if item.get("incoming_rate") and item.base_net_rate < (
|
||||||
|
valuation_rate := flt(
|
||||||
if not valuation_rate_map:
|
item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||||
return
|
)
|
||||||
|
):
|
||||||
or_conditions = (
|
|
||||||
f"""(item_code = {frappe.db.escape(valuation_rate[0])}
|
|
||||||
and warehouse = {frappe.db.escape(valuation_rate[1])})"""
|
|
||||||
for valuation_rate in valuation_rate_map
|
|
||||||
)
|
|
||||||
|
|
||||||
valuation_rates = frappe.db.sql(
|
|
||||||
f"""
|
|
||||||
select
|
|
||||||
item_code, warehouse, valuation_rate
|
|
||||||
from
|
|
||||||
`tabBin`
|
|
||||||
where
|
|
||||||
({" or ".join(or_conditions)})
|
|
||||||
and valuation_rate > 0
|
|
||||||
""",
|
|
||||||
as_dict=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for rate in valuation_rates:
|
|
||||||
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
|
|
||||||
|
|
||||||
for item in self.items:
|
|
||||||
if not item.item_code or item.is_free_item:
|
|
||||||
continue
|
|
||||||
|
|
||||||
last_valuation_rate = valuation_rate_map.get((item.item_code, item.warehouse))
|
|
||||||
|
|
||||||
if not last_valuation_rate:
|
|
||||||
continue
|
|
||||||
|
|
||||||
last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
|
|
||||||
|
|
||||||
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
|
|
||||||
throw_message(
|
throw_message(
|
||||||
item.idx,
|
item.idx,
|
||||||
item.item_name,
|
item.item_name,
|
||||||
last_valuation_rate_in_sales_uom,
|
valuation_rate,
|
||||||
"valuation rate (Moving Average)",
|
"valuation rate",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_item_list(self):
|
def get_item_list(self):
|
||||||
@@ -518,6 +486,8 @@ class SellingController(StockController):
|
|||||||
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||||
|
|
||||||
allow_at_arms_length_price = frappe.get_cached_value(
|
allow_at_arms_length_price = frappe.get_cached_value(
|
||||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||||
)
|
)
|
||||||
@@ -525,6 +495,7 @@ class SellingController(StockController):
|
|||||||
"Selling Settings", "set_zero_rate_for_expired_batch"
|
"Selling Settings", "set_zero_rate_for_expired_batch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
old_doc = self.get_doc_before_save()
|
||||||
items = self.get("items") + (self.get("packed_items") or [])
|
items = self.get("items") + (self.get("packed_items") or [])
|
||||||
for d in items:
|
for d in items:
|
||||||
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
||||||
@@ -554,6 +525,29 @@ class SellingController(StockController):
|
|||||||
# Get incoming rate based on original item cost based on valuation method
|
# 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"))
|
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||||
|
|
||||||
|
if old_doc:
|
||||||
|
old_item = next(
|
||||||
|
(
|
||||||
|
item
|
||||||
|
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
|
||||||
|
if item.name == d.name
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if old_item:
|
||||||
|
old_qty = flt(
|
||||||
|
old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
old_item.item_code != d.item_code
|
||||||
|
or old_item.warehouse != d.warehouse
|
||||||
|
or old_qty != qty
|
||||||
|
or old_item.batch_no != d.batch_no
|
||||||
|
or get_batch_nos(old_item.serial_and_batch_bundle)
|
||||||
|
!= get_batch_nos(d.serial_and_batch_bundle)
|
||||||
|
):
|
||||||
|
d.incoming_rate = 0
|
||||||
|
|
||||||
if (
|
if (
|
||||||
not d.incoming_rate
|
not d.incoming_rate
|
||||||
or self.is_internal_transfer()
|
or self.is_internal_transfer()
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ status_map = {
|
|||||||
],
|
],
|
||||||
"Delivery Note": [
|
"Delivery Note": [
|
||||||
["Draft", None],
|
["Draft", None],
|
||||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
|
||||||
|
["Partially Billed", "eval:self.per_billed < 100 and self.per_billed > 0 and self.docstatus == 1"],
|
||||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||||
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
|
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
|
||||||
@@ -341,14 +342,17 @@ class StatusUpdater(Document):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
if qty_or_amount == "qty":
|
if args["target_dt"] != "Quotation Item":
|
||||||
action_msg = _(
|
if qty_or_amount == "qty":
|
||||||
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
action_msg = _(
|
||||||
)
|
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
action_msg = _(
|
||||||
|
'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
action_msg = _(
|
action_msg = None
|
||||||
'To allow over billing, update "Over Billing Allowance" in Accounts Settings or the Item.'
|
|
||||||
)
|
|
||||||
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
@@ -360,8 +364,7 @@ class StatusUpdater(Document):
|
|||||||
frappe.bold(_(self.doctype)),
|
frappe.bold(_(self.doctype)),
|
||||||
frappe.bold(item.get("item_code")),
|
frappe.bold(item.get("item_code")),
|
||||||
)
|
)
|
||||||
+ "<br><br>"
|
+ ("<br><br>" + action_msg if action_msg else ""),
|
||||||
+ action_msg,
|
|
||||||
OverAllowanceError,
|
OverAllowanceError,
|
||||||
title=_("Limit Crossed"),
|
title=_("Limit Crossed"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -465,7 +465,10 @@ class StockController(AccountsController):
|
|||||||
if is_rejected:
|
if is_rejected:
|
||||||
serial_nos = row.get("rejected_serial_no")
|
serial_nos = row.get("rejected_serial_no")
|
||||||
type_of_transaction = "Inward" if not self.is_return else "Outward"
|
type_of_transaction = "Inward" if not self.is_return else "Outward"
|
||||||
qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0)
|
qty = flt(
|
||||||
|
row.get("rejected_qty") * row.get("conversion_factor", 1.0),
|
||||||
|
frappe.get_precision("Serial and Batch Entry", "qty"),
|
||||||
|
)
|
||||||
warehouse = row.get("rejected_warehouse")
|
warehouse = row.get("rejected_warehouse")
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -254,10 +254,10 @@ class SubcontractingController(StockController):
|
|||||||
):
|
):
|
||||||
for row in frappe.get_all(
|
for row in frappe.get_all(
|
||||||
f"{self.subcontract_data.order_doctype} Item",
|
f"{self.subcontract_data.order_doctype} Item",
|
||||||
fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
|
fields=["item_code", "(qty - received_qty) as qty", "parent", "bom"],
|
||||||
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
|
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
|
||||||
):
|
):
|
||||||
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
|
self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty
|
||||||
|
|
||||||
def __get_transferred_items(self):
|
def __get_transferred_items(self):
|
||||||
se = frappe.qb.DocType("Stock Entry")
|
se = frappe.qb.DocType("Stock Entry")
|
||||||
@@ -829,13 +829,17 @@ class SubcontractingController(StockController):
|
|||||||
self.__set_serial_nos(item_row, rm_obj)
|
self.__set_serial_nos(item_row, rm_obj)
|
||||||
|
|
||||||
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
||||||
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
key = (
|
||||||
|
item_row.item_code,
|
||||||
|
item_row.get(self.subcontract_data.order_field),
|
||||||
|
item_row.get("bom"),
|
||||||
|
)
|
||||||
|
|
||||||
if self.qty_to_be_received == item_row.qty:
|
if self.qty_to_be_received == item_row.qty:
|
||||||
return transfer_item.qty
|
return transfer_item.qty
|
||||||
|
|
||||||
if self.qty_to_be_received:
|
if self.qty_to_be_received.get(key):
|
||||||
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
|
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key))
|
||||||
transfer_item.item_details.required_qty = transfer_item.qty
|
transfer_item.item_details.required_qty = transfer_item.qty
|
||||||
|
|
||||||
if transfer_item.serial_no or frappe.get_cached_value(
|
if transfer_item.serial_no or frappe.get_cached_value(
|
||||||
@@ -880,7 +884,11 @@ class SubcontractingController(StockController):
|
|||||||
|
|
||||||
if self.qty_to_be_received:
|
if self.qty_to_be_received:
|
||||||
self.qty_to_be_received[
|
self.qty_to_be_received[
|
||||||
(row.item_code, row.get(self.subcontract_data.order_field))
|
(
|
||||||
|
row.item_code,
|
||||||
|
row.get(self.subcontract_data.order_field),
|
||||||
|
row.get("bom"),
|
||||||
|
)
|
||||||
] -= row.qty
|
] -= row.qty
|
||||||
|
|
||||||
def __set_rate_for_serial_and_batch_bundle(self):
|
def __set_rate_for_serial_and_batch_bundle(self):
|
||||||
|
|||||||
@@ -701,19 +701,21 @@ class ProductionPlan(Document):
|
|||||||
"project": self.project,
|
"project": self.project,
|
||||||
}
|
}
|
||||||
|
|
||||||
key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse)
|
key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse, d.planned_start_date)
|
||||||
if self.combine_items:
|
if self.combine_items:
|
||||||
key = (d.item_code, d.sales_order, d.warehouse)
|
key = (d.item_code, d.sales_order, d.warehouse, d.planned_start_date)
|
||||||
|
|
||||||
if not d.sales_order:
|
if not d.sales_order:
|
||||||
key = (d.name, d.item_code, d.warehouse)
|
key = (d.name, d.item_code, d.warehouse, d.planned_start_date)
|
||||||
|
|
||||||
if not item_details["project"] and d.sales_order:
|
if not item_details["project"] and d.sales_order:
|
||||||
item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
|
item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project")
|
||||||
|
|
||||||
if self.get_items_from == "Material Request":
|
if self.get_items_from == "Material Request":
|
||||||
item_details.update({"qty": d.planned_qty})
|
item_details.update({"qty": d.planned_qty})
|
||||||
item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
|
item_dict[
|
||||||
|
(d.item_code, d.material_request_item, d.warehouse, d.planned_start_date)
|
||||||
|
] = item_details
|
||||||
else:
|
else:
|
||||||
item_details.update(
|
item_details.update(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -867,7 +867,7 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
items_data = pln.get_production_items()
|
items_data = pln.get_production_items()
|
||||||
|
|
||||||
# Update qty
|
# Update qty
|
||||||
items_data[(pln.po_items[0].name, item, None)]["qty"] = qty
|
items_data[(pln.po_items[0].name, item, None, pln.po_items[0].planned_start_date)]["qty"] = qty
|
||||||
|
|
||||||
# Create and Submit Work Order for each item in items_data
|
# Create and Submit Work Order for each item in items_data
|
||||||
for _key, item in items_data.items():
|
for _key, item in items_data.items():
|
||||||
|
|||||||
@@ -664,7 +664,7 @@ erpnext.work_order = {
|
|||||||
set_custom_buttons: function (frm) {
|
set_custom_buttons: function (frm) {
|
||||||
var doc = frm.doc;
|
var doc = frm.doc;
|
||||||
|
|
||||||
if (doc.docstatus === 1 && doc.status !== "Closed") {
|
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Close"),
|
__("Close"),
|
||||||
function () {
|
function () {
|
||||||
@@ -674,9 +674,6 @@ erpnext.work_order = {
|
|||||||
},
|
},
|
||||||
__("Status")
|
__("Status")
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
|
|
||||||
if (doc.status != "Stopped" && doc.status != "Completed") {
|
if (doc.status != "Stopped" && doc.status != "Completed") {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Stop"),
|
__("Stop"),
|
||||||
|
|||||||
@@ -428,3 +428,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
|
|||||||
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
|
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
|
||||||
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11
|
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11
|
||||||
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
|
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges
|
||||||
|
erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
|
||||||
|
|||||||
16
erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py
Normal file
16
erpnext/patches/v16_0/set_ordered_qty_in_quotation_item.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
data = frappe.get_all(
|
||||||
|
"Sales Order Item",
|
||||||
|
filters={"quotation_item": ["is", "set"], "docstatus": 1},
|
||||||
|
fields=["quotation_item", "sum(stock_qty) as ordered_qty"],
|
||||||
|
group_by="quotation_item",
|
||||||
|
)
|
||||||
|
if data:
|
||||||
|
frappe.db.auto_commit_on_many_writes = 1
|
||||||
|
frappe.db.bulk_update(
|
||||||
|
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
|
||||||
|
)
|
||||||
|
frappe.db.auto_commit_on_many_writes = 0
|
||||||
@@ -308,6 +308,8 @@ class Project(Document):
|
|||||||
self.gross_margin = flt(self.total_billed_amount) - expense_amount
|
self.gross_margin = flt(self.total_billed_amount) - expense_amount
|
||||||
if self.total_billed_amount:
|
if self.total_billed_amount:
|
||||||
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
|
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
|
||||||
|
else:
|
||||||
|
self.per_gross_margin = 0
|
||||||
|
|
||||||
def update_purchase_costing(self):
|
def update_purchase_costing(self):
|
||||||
total_purchase_cost = calculate_total_purchase_cost(self.name)
|
total_purchase_cost = calculate_total_purchase_cost(self.name)
|
||||||
|
|||||||
@@ -138,14 +138,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
|
|
||||||
frappe.run_serially([
|
frappe.run_serially([
|
||||||
() => this.set_selector_trigger_flag(data),
|
() => this.set_selector_trigger_flag(data),
|
||||||
() => this.set_serial_no(row, serial_no),
|
|
||||||
() => this.set_batch_no(row, batch_no),
|
|
||||||
() => this.set_barcode(row, barcode),
|
() => this.set_barcode(row, barcode),
|
||||||
() => this.set_warehouse(row),
|
() => this.set_warehouse(row),
|
||||||
() =>
|
() =>
|
||||||
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
|
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
|
||||||
this.show_scan_message(row.idx, !is_new_row, qty);
|
this.show_scan_message(row.idx, !is_new_row, qty);
|
||||||
}),
|
}),
|
||||||
|
() => this.set_serial_no(row, serial_no),
|
||||||
|
() => this.set_batch_no(row, batch_no),
|
||||||
() => this.clean_up(),
|
() => this.clean_up(),
|
||||||
() => this.set_barcode_uom(row, uom),
|
() => this.set_barcode_uom(row, uom),
|
||||||
() => this.revert_selector_flag(),
|
() => this.revert_selector_flag(),
|
||||||
|
|||||||
@@ -497,6 +497,9 @@ def _set_missing_values(source, target):
|
|||||||
|
|
||||||
if contact:
|
if contact:
|
||||||
target.contact_person = contact[0].parent
|
target.contact_person = contact[0].parent
|
||||||
|
target.contact_display, target.contact_email, target.contact_mobile = frappe.get_value(
|
||||||
|
"Contact", contact[0].parent, ["full_name", "email_id", "mobile_no"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -446,7 +446,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
|
|||||||
"Quotation",
|
"Quotation",
|
||||||
source_name,
|
source_name,
|
||||||
{
|
{
|
||||||
"Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}},
|
"Quotation": {
|
||||||
|
"doctype": "Sales Order",
|
||||||
|
"validation": {"docstatus": ["=", 1]},
|
||||||
|
},
|
||||||
"Quotation Item": {
|
"Quotation Item": {
|
||||||
"doctype": "Sales Order Item",
|
"doctype": "Sales Order Item",
|
||||||
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
|
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
|
||||||
@@ -549,6 +552,8 @@ def _make_customer(source_name, ignore_permissions=False):
|
|||||||
|
|
||||||
if quotation.quotation_to == "Customer":
|
if quotation.quotation_to == "Customer":
|
||||||
return frappe.get_doc("Customer", quotation.party_name)
|
return frappe.get_doc("Customer", quotation.party_name)
|
||||||
|
elif quotation.quotation_to == "CRM Deal":
|
||||||
|
return frappe.get_doc("Customer", {"crm_deal": quotation.party_name})
|
||||||
|
|
||||||
# Check if a Customer already exists for the Lead or Prospect.
|
# Check if a Customer already exists for the Lead or Prospect.
|
||||||
existing_customer = None
|
existing_customer = None
|
||||||
@@ -610,25 +615,8 @@ def handle_mandatory_error(e, customer, lead_name):
|
|||||||
|
|
||||||
|
|
||||||
def get_ordered_items(quotation: str):
|
def get_ordered_items(quotation: str):
|
||||||
"""
|
|
||||||
Returns a dict of ordered items with their total qty based on quotation row name.
|
|
||||||
|
|
||||||
In `Sales Order Item`, `quotation_item` is the row name of `Quotation Item`.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"refsdjhd2": 10,
|
|
||||||
"ygdhdshrt": 5,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
return frappe._dict(
|
return frappe._dict(
|
||||||
frappe.get_all(
|
frappe.get_all(
|
||||||
"Sales Order Item",
|
"Quotation Item", {"docstatus": 1, "parent": quotation}, ["name", "ordered_qty"], as_list=True
|
||||||
filters={"prevdoc_docname": quotation, "docstatus": 1},
|
|
||||||
fields=["quotation_item", "sum(qty)"],
|
|
||||||
group_by="quotation_item",
|
|
||||||
as_list=1,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -828,7 +828,7 @@ class TestQuotation(FrappeTestCase):
|
|||||||
# item code same but description different
|
# item code same but description different
|
||||||
make_item("_Test Item 2", {"is_stock_item": 1})
|
make_item("_Test Item 2", {"is_stock_item": 1})
|
||||||
|
|
||||||
quotation = make_quotation(qty=1, rate=100, do_not_submit=1)
|
quotation = make_quotation(qty=10, rate=100, do_not_submit=1)
|
||||||
|
|
||||||
# duplicate items
|
# duplicate items
|
||||||
for qty in [1, 1, 2, 3]:
|
for qty in [1, 1, 2, 3]:
|
||||||
@@ -842,7 +842,7 @@ class TestQuotation(FrappeTestCase):
|
|||||||
sales_order.delivery_date = nowdate()
|
sales_order.delivery_date = nowdate()
|
||||||
|
|
||||||
self.assertEqual(len(sales_order.items), 6)
|
self.assertEqual(len(sales_order.items), 6)
|
||||||
self.assertEqual(sales_order.items[0].qty, 1)
|
self.assertEqual(sales_order.items[0].qty, 10)
|
||||||
self.assertEqual(sales_order.items[-1].qty, 5)
|
self.assertEqual(sales_order.items[-1].qty, 5)
|
||||||
|
|
||||||
# Row 1: 10, Row 4: 1, Row 5: 1
|
# Row 1: 10, Row 4: 1, Row 5: 1
|
||||||
@@ -885,6 +885,18 @@ class TestQuotation(FrappeTestCase):
|
|||||||
f"Expected conversion rate {expected_rate}, got {quotation.conversion_rate}",
|
f"Expected conversion rate {expected_rate}, got {quotation.conversion_rate}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_over_order_limit(self):
|
||||||
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
|
|
||||||
|
quotation = make_quotation(qty=5)
|
||||||
|
so1 = make_sales_order(quotation.name)
|
||||||
|
so2 = make_sales_order(quotation.name)
|
||||||
|
so1.delivery_date = nowdate()
|
||||||
|
so2.delivery_date = nowdate()
|
||||||
|
|
||||||
|
so1.submit()
|
||||||
|
self.assertRaises(frappe.ValidationError, so2.submit)
|
||||||
|
|
||||||
|
|
||||||
test_records = frappe.get_test_records("Quotation")
|
test_records = frappe.get_test_records("Quotation")
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"uom",
|
"uom",
|
||||||
"conversion_factor",
|
"conversion_factor",
|
||||||
"stock_qty",
|
"stock_qty",
|
||||||
|
"ordered_qty",
|
||||||
"available_quantity_section",
|
"available_quantity_section",
|
||||||
"actual_qty",
|
"actual_qty",
|
||||||
"column_break_ylrv",
|
"column_break_ylrv",
|
||||||
@@ -694,12 +695,23 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "ordered_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Ordered Qty",
|
||||||
|
"no_copy": 1,
|
||||||
|
"non_negative": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-26 20:31:47.775890",
|
"modified": "2026-01-30 12:56:08.320190",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Quotation Item",
|
"name": "Quotation Item",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class QuotationItem(Document):
|
|||||||
margin_type: DF.Literal["", "Percentage", "Amount"]
|
margin_type: DF.Literal["", "Percentage", "Amount"]
|
||||||
net_amount: DF.Currency
|
net_amount: DF.Currency
|
||||||
net_rate: DF.Currency
|
net_rate: DF.Currency
|
||||||
|
ordered_qty: DF.Float
|
||||||
page_break: DF.Check
|
page_break: DF.Check
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
|
|||||||
@@ -185,6 +185,16 @@ class SalesOrder(SellingController):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.status_updater = [
|
||||||
|
{
|
||||||
|
"source_dt": "Sales Order Item",
|
||||||
|
"target_dt": "Quotation Item",
|
||||||
|
"join_field": "quotation_item",
|
||||||
|
"target_field": "ordered_qty",
|
||||||
|
"target_ref_field": "stock_qty",
|
||||||
|
"source_field": "stock_qty",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def onload(self) -> None:
|
def onload(self) -> None:
|
||||||
super().onload()
|
super().onload()
|
||||||
@@ -419,6 +429,7 @@ class SalesOrder(SellingController):
|
|||||||
frappe.throw(_("Row #{0}: Set Supplier for item {1}").format(d.idx, d.item_code))
|
frappe.throw(_("Row #{0}: Set Supplier for item {1}").format(d.idx, d.item_code))
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
super().update_prevdoc_status()
|
||||||
self.check_credit_limit()
|
self.check_credit_limit()
|
||||||
self.update_reserved_qty()
|
self.update_reserved_qty()
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
@change_settings("Selling Settings", {"allow_negative_rates_for_items": 1})
|
||||||
def test_sales_order_with_negative_rate(self):
|
def test_sales_order_with_negative_rate(self):
|
||||||
"""
|
"""
|
||||||
Test if negative rate is allowed in Sales Order via doc submission and update items
|
Test if negative rate is allowed in Sales Order via doc submission and update items
|
||||||
|
|||||||
@@ -4844,17 +4844,17 @@
|
|||||||
|
|
||||||
"Switzerland": {
|
"Switzerland": {
|
||||||
"Switzerland normal VAT": {
|
"Switzerland normal VAT": {
|
||||||
"account_name": "VAT 7.7%",
|
"account_name": "VAT 8.1%",
|
||||||
"tax_rate": 7.70,
|
"tax_rate": 8.10,
|
||||||
"default": 1
|
"default": 1
|
||||||
},
|
},
|
||||||
"Switzerland reduced VAT": {
|
"Switzerland reduced VAT": {
|
||||||
"account_name": "VAT 2.5%",
|
"account_name": "VAT 2.6%",
|
||||||
"tax_rate": 2.50
|
"tax_rate": 2.60
|
||||||
},
|
},
|
||||||
"Switzerland lodging VAT": {
|
"Switzerland lodging VAT": {
|
||||||
"account_name": "VAT 3.7%",
|
"account_name": "VAT 3.8%",
|
||||||
"tax_rate": 3.70
|
"tax_rate": 3.80
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1091,7 +1091,7 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"oldfieldname": "status",
|
"oldfieldname": "status",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "\nDraft\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
|
"options": "\nDraft\nTo Bill\nPartially Billed\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
@@ -1404,7 +1404,7 @@
|
|||||||
"idx": 146,
|
"idx": 146,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-12-02 23:55:25.415443",
|
"modified": "2026-02-03 12:27:19.055918",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note",
|
"name": "Delivery Note",
|
||||||
|
|||||||
@@ -127,7 +127,15 @@ class DeliveryNote(SellingController):
|
|||||||
shipping_rule: DF.Link | None
|
shipping_rule: DF.Link | None
|
||||||
source: DF.Link | None
|
source: DF.Link | None
|
||||||
status: DF.Literal[
|
status: DF.Literal[
|
||||||
"", "Draft", "To Bill", "Completed", "Return", "Return Issued", "Cancelled", "Closed"
|
"",
|
||||||
|
"Draft",
|
||||||
|
"To Bill",
|
||||||
|
"Partially Billed",
|
||||||
|
"Completed",
|
||||||
|
"Return",
|
||||||
|
"Return Issued",
|
||||||
|
"Cancelled",
|
||||||
|
"Closed",
|
||||||
]
|
]
|
||||||
tax_category: DF.Link | None
|
tax_category: DF.Link | None
|
||||||
tax_id: DF.Data | None
|
tax_id: DF.Data | None
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ frappe.listview_settings["Delivery Note"] = {
|
|||||||
return [__("Closed"), "green", "status,=,Closed"];
|
return [__("Closed"), "green", "status,=,Closed"];
|
||||||
} else if (doc.status === "Return Issued") {
|
} else if (doc.status === "Return Issued") {
|
||||||
return [__("Return Issued"), "grey", "status,=,Return Issued"];
|
return [__("Return Issued"), "grey", "status,=,Return Issued"];
|
||||||
} else if (flt(doc.per_billed, 2) < 100) {
|
} else if (flt(doc.per_billed) == 0) {
|
||||||
return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
|
return [__("To Bill"), "orange", "per_billed,=,0|docstatus,=,1"];
|
||||||
|
} else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
|
||||||
|
return [__("Partially Billed"), "yellow", "per_billed,<,100|docstatus,=,1"];
|
||||||
} else if (flt(doc.per_billed, 2) === 100) {
|
} else if (flt(doc.per_billed, 2) === 100) {
|
||||||
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
|
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1093,7 +1093,8 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(dn2.get("items")[0].billed_amt, 400)
|
self.assertEqual(dn2.get("items")[0].billed_amt, 400)
|
||||||
self.assertEqual(dn2.per_billed, 80)
|
self.assertEqual(dn2.per_billed, 80)
|
||||||
self.assertEqual(dn2.status, "To Bill")
|
# Since 20% of DN2 is yet to be billed, it should be classified as partially billed.
|
||||||
|
self.assertEqual(dn2.status, "Partially Billed")
|
||||||
|
|
||||||
def test_dn_billing_status_case4(self):
|
def test_dn_billing_status_case4(self):
|
||||||
# SO -> SI -> DN
|
# SO -> SI -> DN
|
||||||
@@ -2807,6 +2808,23 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
|
|
||||||
frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision)
|
frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision)
|
||||||
|
|
||||||
|
@change_settings("Selling Settings", {"validate_selling_price": 1})
|
||||||
|
def test_validate_selling_price(self):
|
||||||
|
item_code = make_item("VSP Item", properties={"is_stock_item": 1}).name
|
||||||
|
make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=10)
|
||||||
|
make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=1)
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=1,
|
||||||
|
rate=9,
|
||||||
|
do_not_save=True,
|
||||||
|
)
|
||||||
|
self.assertRaises(frappe.ValidationError, dn.save)
|
||||||
|
dn.items[0].incoming_rate = 0
|
||||||
|
dn.items[0].stock_qty = 2
|
||||||
|
dn.save()
|
||||||
|
|
||||||
|
|
||||||
def create_delivery_note(**args):
|
def create_delivery_note(**args):
|
||||||
dn = frappe.new_doc("Delivery Note")
|
dn = frappe.new_doc("Delivery Note")
|
||||||
|
|||||||
@@ -4794,6 +4794,128 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(stk_ledger.incoming_rate, 120)
|
self.assertEqual(stk_ledger.incoming_rate, 120)
|
||||||
self.assertEqual(stk_ledger.stock_value_difference, 600)
|
self.assertEqual(stk_ledger.stock_value_difference, 600)
|
||||||
|
|
||||||
|
def test_negative_stock_error_for_purchase_return_when_stock_exists_in_future_date(self):
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
from erpnext.stock.stock_ledger import NegativeStockError
|
||||||
|
|
||||||
|
item_code = make_item(
|
||||||
|
"Test Negative Stock for Purchase Return with Future Stock Item",
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TNSPFPRI.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
posting_date=add_days(today(), -4),
|
||||||
|
qty=100,
|
||||||
|
rate=100,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
pr1 = make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
posting_date=add_days(today(), -3),
|
||||||
|
qty=100,
|
||||||
|
rate=100,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
pr2 = make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
posting_date=add_days(today(), -2),
|
||||||
|
qty=100,
|
||||||
|
rate=100,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
batch2 = get_batch_from_bundle(pr2.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=100,
|
||||||
|
posting_date=add_days(today(), -1),
|
||||||
|
source="_Test Warehouse - _TC",
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
batch_no=batch1,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=100,
|
||||||
|
posting_date=add_days(today(), -1),
|
||||||
|
source="_Test Warehouse - _TC",
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
batch_no=batch2,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=100,
|
||||||
|
posting_date=today(),
|
||||||
|
source="_Test Warehouse 1 - _TC",
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
batch_no=batch1,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
|
||||||
|
make_purchase_entry.set_posting_time = 1
|
||||||
|
make_purchase_entry.posting_date = pr1.posting_date
|
||||||
|
self.assertRaises(NegativeStockError, make_purchase_entry.submit)
|
||||||
|
|
||||||
|
def test_purchase_return_from_different_warehouse(self):
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
item_code = make_item(
|
||||||
|
"Test Purchase Return From Different Warehouse Item",
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TPRFDWU.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
pr1 = make_purchase_receipt(
|
||||||
|
item_code=item_code,
|
||||||
|
posting_date=add_days(today(), -4),
|
||||||
|
qty=100,
|
||||||
|
rate=100,
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
)
|
||||||
|
|
||||||
|
batch1 = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=100,
|
||||||
|
posting_date=add_days(today(), -1),
|
||||||
|
source="_Test Warehouse - _TC",
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
batch_no=batch1,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
make_purchase_entry = make_return_doc("Purchase Receipt", pr1.name)
|
||||||
|
make_purchase_entry.items[0].warehouse = "_Test Warehouse 1 - _TC"
|
||||||
|
make_purchase_entry.submit()
|
||||||
|
make_purchase_entry.reload()
|
||||||
|
|
||||||
|
sabb = frappe.get_doc("Serial and Batch Bundle", make_purchase_entry.items[0].serial_and_batch_bundle)
|
||||||
|
for row in sabb.entries:
|
||||||
|
self.assertEqual(row.warehouse, "_Test Warehouse 1 - _TC")
|
||||||
|
self.assertEqual(row.incoming_rate, 100)
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from frappe.utils import (
|
|||||||
cint,
|
cint,
|
||||||
cstr,
|
cstr,
|
||||||
flt,
|
flt,
|
||||||
|
get_datetime,
|
||||||
get_link_to_form,
|
get_link_to_form,
|
||||||
getdate,
|
getdate,
|
||||||
now,
|
now,
|
||||||
@@ -439,6 +440,8 @@ class SerialandBatchBundle(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_valuation_rate_for_return_entry(self, return_against):
|
def get_valuation_rate_for_return_entry(self, return_against):
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import get_warehouses_for_return
|
||||||
|
|
||||||
if not self.voucher_detail_no:
|
if not self.voucher_detail_no:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -468,9 +471,11 @@ class SerialandBatchBundle(Document):
|
|||||||
["Serial and Batch Bundle", "voucher_detail_no", "=", return_against_voucher_detail_no],
|
["Serial and Batch Bundle", "voucher_detail_no", "=", return_against_voucher_detail_no],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Added to handle rejected warehouse case
|
||||||
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
|
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
|
||||||
# Added to handle rejected warehouse case
|
warehouses = get_warehouses_for_return(self.voucher_type, return_against_voucher_detail_no)
|
||||||
filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
|
if self.warehouse in warehouses:
|
||||||
|
filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
|
||||||
|
|
||||||
bundle_data = frappe.get_all(
|
bundle_data = frappe.get_all(
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
@@ -1419,31 +1424,44 @@ class SerialandBatchBundle(Document):
|
|||||||
for d in self.entries:
|
for d in self.entries:
|
||||||
available_qty = batch_wise_available_qty.get(d.batch_no, 0)
|
available_qty = batch_wise_available_qty.get(d.batch_no, 0)
|
||||||
if flt(available_qty, precision) < 0:
|
if flt(available_qty, precision) < 0:
|
||||||
frappe.throw(
|
self.throw_negative_batch(d.batch_no, available_qty, precision)
|
||||||
_(
|
|
||||||
"""
|
def throw_negative_batch(self, batch_no, available_qty, precision):
|
||||||
The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
|
from erpnext.stock.stock_ledger import NegativeStockError
|
||||||
).format(
|
|
||||||
bold(d.batch_no),
|
frappe.throw(
|
||||||
bold(self.item_code),
|
_(
|
||||||
bold(self.warehouse),
|
"""
|
||||||
bold(abs(flt(available_qty, precision))),
|
The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
|
||||||
),
|
).format(
|
||||||
title=_("Negative Stock Error"),
|
bold(batch_no),
|
||||||
)
|
bold(self.item_code),
|
||||||
|
bold(self.warehouse),
|
||||||
|
bold(abs(flt(available_qty, precision))),
|
||||||
|
),
|
||||||
|
title=_("Negative Stock Error"),
|
||||||
|
exc=NegativeStockError,
|
||||||
|
)
|
||||||
|
|
||||||
def get_batchwise_available_qty(self):
|
def get_batchwise_available_qty(self):
|
||||||
available_qty = self.get_available_qty_from_sabb()
|
batchwise_entries = self.get_available_qty_from_sabb()
|
||||||
available_qty_from_ledger = self.get_available_qty_from_stock_ledger()
|
batchwise_entries.extend(self.get_available_qty_from_stock_ledger())
|
||||||
|
|
||||||
if not available_qty_from_ledger:
|
available_qty = frappe._dict({})
|
||||||
return available_qty
|
batchwise_entries = sorted(
|
||||||
|
batchwise_entries,
|
||||||
|
key=lambda x: (get_datetime(x.get("posting_datetime")), get_datetime(x.get("creation"))),
|
||||||
|
)
|
||||||
|
|
||||||
for batch_no, qty in available_qty_from_ledger.items():
|
precision = frappe.get_precision("Serial and Batch Entry", "qty")
|
||||||
if batch_no in available_qty:
|
for row in batchwise_entries:
|
||||||
available_qty[batch_no] += qty
|
if row.batch_no in available_qty:
|
||||||
|
available_qty[row.batch_no] += flt(row.qty)
|
||||||
else:
|
else:
|
||||||
available_qty[batch_no] = qty
|
available_qty[row.batch_no] = flt(row.qty)
|
||||||
|
|
||||||
|
if flt(available_qty[row.batch_no], precision) < 0:
|
||||||
|
self.throw_negative_batch(row.batch_no, available_qty[row.batch_no], precision)
|
||||||
|
|
||||||
return available_qty
|
return available_qty
|
||||||
|
|
||||||
@@ -1456,7 +1474,9 @@ class SerialandBatchBundle(Document):
|
|||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.select(
|
.select(
|
||||||
sle.batch_no,
|
sle.batch_no,
|
||||||
Sum(sle.actual_qty).as_("available_qty"),
|
sle.actual_qty.as_("qty"),
|
||||||
|
sle.posting_datetime,
|
||||||
|
sle.creation,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(sle.item_code == self.item_code)
|
(sle.item_code == self.item_code)
|
||||||
@@ -1468,12 +1488,9 @@ class SerialandBatchBundle(Document):
|
|||||||
& (sle.batch_no.isnotnull())
|
& (sle.batch_no.isnotnull())
|
||||||
)
|
)
|
||||||
.for_update()
|
.for_update()
|
||||||
.groupby(sle.batch_no)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
res = query.run(as_list=True)
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
return frappe._dict(res) if res else frappe._dict()
|
|
||||||
|
|
||||||
def get_available_qty_from_sabb(self):
|
def get_available_qty_from_sabb(self):
|
||||||
batches = [d.batch_no for d in self.entries if d.batch_no]
|
batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||||
@@ -1487,7 +1504,9 @@ class SerialandBatchBundle(Document):
|
|||||||
.on(parent.name == child.parent)
|
.on(parent.name == child.parent)
|
||||||
.select(
|
.select(
|
||||||
child.batch_no,
|
child.batch_no,
|
||||||
Sum(child.qty).as_("total_qty"),
|
child.qty,
|
||||||
|
CombineDatetime(parent.posting_date, parent.posting_time).as_("posting_datetime"),
|
||||||
|
parent.creation,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(parent.warehouse == self.warehouse)
|
(parent.warehouse == self.warehouse)
|
||||||
@@ -1498,14 +1517,11 @@ class SerialandBatchBundle(Document):
|
|||||||
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
|
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
|
||||||
)
|
)
|
||||||
.for_update()
|
.for_update()
|
||||||
.groupby(child.batch_no)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
query = query.where(parent.voucher_type != "Pick List")
|
query = query.where(parent.voucher_type != "Pick List")
|
||||||
|
|
||||||
res = query.run(as_list=True)
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
return frappe._dict(res) if res else frappe._dict()
|
|
||||||
|
|
||||||
def validate_voucher_no_docstatus(self):
|
def validate_voucher_no_docstatus(self):
|
||||||
if self.voucher_type == "POS Invoice":
|
if self.voucher_type == "POS Invoice":
|
||||||
@@ -2465,11 +2481,11 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
|
|||||||
|
|
||||||
key = (row.batch_no, row.warehouse)
|
key = (row.batch_no, row.warehouse)
|
||||||
if key in pos_batches:
|
if key in pos_batches:
|
||||||
pos_batches[key]["qty"] -= row.qty * -1 if row.is_return else row.qty
|
pos_batches[key]["qty"] += row.qty * -1
|
||||||
else:
|
else:
|
||||||
pos_batches[key] = frappe._dict(
|
pos_batches[key] = frappe._dict(
|
||||||
{
|
{
|
||||||
"qty": (row.qty * -1 if not row.is_return else row.qty),
|
"qty": row.qty * -1,
|
||||||
"warehouse": row.warehouse,
|
"warehouse": row.warehouse,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ class StockEntry(StockController):
|
|||||||
self.validate_job_card_item()
|
self.validate_job_card_item()
|
||||||
self.set_purpose_for_stock_entry()
|
self.set_purpose_for_stock_entry()
|
||||||
self.clean_serial_nos()
|
self.clean_serial_nos()
|
||||||
|
self.validate_repack_entry()
|
||||||
|
|
||||||
if not self.from_bom:
|
if not self.from_bom:
|
||||||
self.fg_completed_qty = 0.0
|
self.fg_completed_qty = 0.0
|
||||||
@@ -245,6 +246,20 @@ class StockEntry(StockController):
|
|||||||
self.validate_same_source_target_warehouse_during_material_transfer()
|
self.validate_same_source_target_warehouse_during_material_transfer()
|
||||||
self.validate_raw_materials_exists()
|
self.validate_raw_materials_exists()
|
||||||
|
|
||||||
|
def validate_repack_entry(self):
|
||||||
|
if self.purpose != "Repack":
|
||||||
|
return
|
||||||
|
|
||||||
|
fg_items = {row.item_code: row for row in self.items if row.is_finished_item}
|
||||||
|
|
||||||
|
if len(fg_items) > 1 and not all(row.set_basic_rate_manually for row in fg_items.values()):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"When there are multiple finished goods ({0}) in a Repack stock entry, the basic rate for all finished goods must be set manually. To set rate manually, enable the checkbox 'Set Basic Rate Manually' in the respective finished good row."
|
||||||
|
).format(", ".join(fg_items)),
|
||||||
|
title=_("Set Basic Rate Manually"),
|
||||||
|
)
|
||||||
|
|
||||||
def validate_raw_materials_exists(self):
|
def validate_raw_materials_exists(self):
|
||||||
if self.purpose not in ["Manufacture", "Repack", "Disassemble"]:
|
if self.purpose not in ["Manufacture", "Repack", "Disassemble"]:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -413,6 +413,10 @@ class TestStockEntry(FrappeTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
repack.set_stock_entry_type()
|
repack.set_stock_entry_type()
|
||||||
|
for row in repack.items:
|
||||||
|
if row.t_warehouse:
|
||||||
|
row.set_basic_rate_manually = 1
|
||||||
|
|
||||||
repack.insert()
|
repack.insert()
|
||||||
|
|
||||||
self.assertEqual(repack.items[1].is_finished_item, 1)
|
self.assertEqual(repack.items[1].is_finished_item, 1)
|
||||||
|
|||||||
@@ -1266,15 +1266,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig
|
|||||||
|
|
||||||
for d in items:
|
for d in items:
|
||||||
if (d.item_code, d.warehouse) in itemwise_batch_data:
|
if (d.item_code, d.warehouse) in itemwise_batch_data:
|
||||||
valuation_rate = get_stock_balance(
|
|
||||||
d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True
|
|
||||||
)[1]
|
|
||||||
|
|
||||||
for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
|
for row in itemwise_batch_data.get((d.item_code, d.warehouse)):
|
||||||
if ignore_empty_stock and not row.qty:
|
if ignore_empty_stock and not row.qty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
args = get_item_data(row, row.qty, valuation_rate)
|
args = get_item_data(row, row.qty, row.valuation_rate)
|
||||||
res.append(args)
|
res.append(args)
|
||||||
else:
|
else:
|
||||||
stock_bal = get_stock_balance(
|
stock_bal = get_stock_balance(
|
||||||
@@ -1408,6 +1404,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
|
|||||||
"item_code": row[0],
|
"item_code": row[0],
|
||||||
"warehouse": row[3],
|
"warehouse": row[3],
|
||||||
"qty": row[8],
|
"qty": row[8],
|
||||||
|
"valuation_rate": row[9],
|
||||||
"item_name": row[1],
|
"item_name": row[1],
|
||||||
"batch_no": row[4],
|
"batch_no": row[4],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,7 +198,11 @@ class StockBalanceReport:
|
|||||||
for field in self.inventory_dimensions:
|
for field in self.inventory_dimensions:
|
||||||
qty_dict[field] = entry.get(field)
|
qty_dict[field] = entry.get(field)
|
||||||
|
|
||||||
if entry.voucher_type == "Stock Reconciliation" and (not entry.batch_no or entry.serial_no):
|
if (
|
||||||
|
entry.voucher_type == "Stock Reconciliation"
|
||||||
|
and frappe.get_cached_value(entry.voucher_type, entry.voucher_no, "purpose") != "Opening Stock"
|
||||||
|
and (not entry.batch_no or entry.serial_no)
|
||||||
|
):
|
||||||
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
|
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
|
||||||
else:
|
else:
|
||||||
qty_diff = flt(entry.actual_qty)
|
qty_diff = flt(entry.actual_qty)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.query_builder.functions import Coalesce, Sum
|
||||||
from frappe.utils import cstr, flt, now, nowdate, nowtime
|
from frappe.utils import cstr, flt, now, nowdate, nowtime
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
|
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
|
||||||
@@ -182,18 +183,67 @@ def get_indented_qty(item_code, warehouse):
|
|||||||
|
|
||||||
|
|
||||||
def get_ordered_qty(item_code, warehouse):
|
def get_ordered_qty(item_code, warehouse):
|
||||||
ordered_qty = frappe.db.sql(
|
"""Return total pending ordered quantity for an item in a warehouse.
|
||||||
"""
|
Includes outstanding quantities from Purchase Orders and Subcontracting Orders"""
|
||||||
select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor)
|
|
||||||
from `tabPurchase Order Item` po_item, `tabPurchase Order` po
|
purchase_order_qty = get_purchase_order_qty(item_code, warehouse)
|
||||||
where po_item.item_code=%s and po_item.warehouse=%s
|
subcontracting_order_qty = get_subcontracting_order_qty(item_code, warehouse)
|
||||||
and po_item.qty > po_item.received_qty and po_item.parent=po.name
|
|
||||||
and po.status not in ('Closed', 'Delivered') and po.docstatus=1
|
return flt(purchase_order_qty) + flt(subcontracting_order_qty)
|
||||||
and po_item.delivered_by_supplier = 0""",
|
|
||||||
(item_code, warehouse),
|
|
||||||
|
def get_purchase_order_qty(item_code, warehouse):
|
||||||
|
PurchaseOrder = frappe.qb.DocType("Purchase Order")
|
||||||
|
PurchaseOrderItem = frappe.qb.DocType("Purchase Order Item")
|
||||||
|
|
||||||
|
purchase_order_qty = (
|
||||||
|
frappe.qb.from_(PurchaseOrderItem)
|
||||||
|
.join(PurchaseOrder)
|
||||||
|
.on(PurchaseOrderItem.parent == PurchaseOrder.name)
|
||||||
|
.select(
|
||||||
|
Sum(
|
||||||
|
(PurchaseOrderItem.qty - PurchaseOrderItem.received_qty) * PurchaseOrderItem.conversion_factor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(PurchaseOrderItem.item_code == item_code)
|
||||||
|
& (PurchaseOrderItem.warehouse == warehouse)
|
||||||
|
& (PurchaseOrderItem.qty > PurchaseOrderItem.received_qty)
|
||||||
|
& (PurchaseOrder.status.notin(["Closed", "Delivered"]))
|
||||||
|
& (PurchaseOrder.docstatus == 1)
|
||||||
|
& (Coalesce(PurchaseOrderItem.delivered_by_supplier, 0) == 0)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
)
|
)
|
||||||
|
|
||||||
return flt(ordered_qty[0][0]) if ordered_qty else 0
|
return purchase_order_qty[0][0] if purchase_order_qty else 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_subcontracting_order_qty(item_code, warehouse):
|
||||||
|
SubcontractingOrder = frappe.qb.DocType("Subcontracting Order")
|
||||||
|
SubcontractingOrderItem = frappe.qb.DocType("Subcontracting Order Item")
|
||||||
|
|
||||||
|
subcontracting_order_qty = (
|
||||||
|
frappe.qb.from_(SubcontractingOrderItem)
|
||||||
|
.join(SubcontractingOrder)
|
||||||
|
.on(SubcontractingOrderItem.parent == SubcontractingOrder.name)
|
||||||
|
.select(
|
||||||
|
Sum(
|
||||||
|
(SubcontractingOrderItem.qty - SubcontractingOrderItem.received_qty)
|
||||||
|
* SubcontractingOrderItem.conversion_factor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(SubcontractingOrderItem.item_code == item_code)
|
||||||
|
& (SubcontractingOrderItem.warehouse == warehouse)
|
||||||
|
& (SubcontractingOrderItem.qty > SubcontractingOrderItem.received_qty)
|
||||||
|
& (SubcontractingOrder.status.notin(["Closed", "Completed"]))
|
||||||
|
& (SubcontractingOrder.docstatus == 1)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
|
||||||
|
return subcontracting_order_qty[0][0] if subcontracting_order_qty else 0
|
||||||
|
|
||||||
|
|
||||||
def get_planned_qty(item_code, warehouse):
|
def get_planned_qty(item_code, warehouse):
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from frappe.utils import flt
|
|||||||
|
|
||||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||||
from erpnext.stock.stock_balance import update_bin_qty
|
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
|
||||||
from erpnext.stock.utils import get_bin
|
from erpnext.stock.utils import get_bin
|
||||||
|
|
||||||
|
|
||||||
@@ -211,30 +211,7 @@ class SubcontractingOrder(SubcontractingController):
|
|||||||
):
|
):
|
||||||
item_wh_list.append([item.item_code, item.warehouse])
|
item_wh_list.append([item.item_code, item.warehouse])
|
||||||
for item_code, warehouse in item_wh_list:
|
for item_code, warehouse in item_wh_list:
|
||||||
update_bin_qty(item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)})
|
update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_ordered_qty(item_code, warehouse):
|
|
||||||
table = frappe.qb.DocType("Subcontracting Order")
|
|
||||||
child = frappe.qb.DocType("Subcontracting Order Item")
|
|
||||||
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(table)
|
|
||||||
.inner_join(child)
|
|
||||||
.on(table.name == child.parent)
|
|
||||||
.select((child.qty - child.received_qty) * child.conversion_factor)
|
|
||||||
.where(
|
|
||||||
(table.docstatus == 1)
|
|
||||||
& (child.item_code == item_code)
|
|
||||||
& (child.warehouse == warehouse)
|
|
||||||
& (child.qty > child.received_qty)
|
|
||||||
& (table.status != "Completed")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
query = query.run()
|
|
||||||
|
|
||||||
return flt(query[0][0]) if query else 0
|
|
||||||
|
|
||||||
def update_reserved_qty_for_subcontracting(self, sco_item_rows=None):
|
def update_reserved_qty_for_subcontracting(self, sco_item_rows=None):
|
||||||
for item in self.supplied_items:
|
for item in self.supplied_items:
|
||||||
|
|||||||
@@ -618,6 +618,117 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
for item in scr.supplied_items:
|
for item in scr.supplied_items:
|
||||||
self.assertFalse(item.available_qty_for_consumption)
|
self.assertFalse(item.available_qty_for_consumption)
|
||||||
|
|
||||||
|
def test_supplied_items_consumed_qty_for_similar_finished_goods(self):
|
||||||
|
"""
|
||||||
|
Test that supplied raw material consumption is calculated correctly
|
||||||
|
when multiple subcontracted service items use the same finished good
|
||||||
|
but different BOMs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from erpnext.controllers.subcontracting_controller import (
|
||||||
|
make_rm_stock_entry as make_subcontract_transfer_entry,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
|
||||||
|
# Configuration: Backflush based on subcontract material transfer
|
||||||
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
|
|
||||||
|
# Create Raw Materials
|
||||||
|
raw_material_1 = make_item("_RM Item 1", properties={"is_stock_item": 1}).name
|
||||||
|
|
||||||
|
raw_material_2 = make_item("_RM Item 2", properties={"is_stock_item": 1}).name
|
||||||
|
|
||||||
|
# Create Subcontracted Finished Good
|
||||||
|
finished_good = make_item("_Finished Good Item", properties={"is_stock_item": 1})
|
||||||
|
finished_good.is_sub_contracted_item = 1
|
||||||
|
finished_good.save()
|
||||||
|
|
||||||
|
# Receive Raw Materials into Warehouse
|
||||||
|
for raw_material in (raw_material_1, raw_material_2):
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=raw_material,
|
||||||
|
qty=10,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create BOMs for the same Finished Good with different RMs
|
||||||
|
bom_rm_1 = make_bom(
|
||||||
|
item=finished_good.name,
|
||||||
|
quantity=1,
|
||||||
|
raw_materials=[raw_material_1],
|
||||||
|
).name
|
||||||
|
|
||||||
|
_bom_rm_2 = make_bom(
|
||||||
|
item=finished_good.name,
|
||||||
|
quantity=1,
|
||||||
|
raw_materials=[raw_material_2],
|
||||||
|
).name
|
||||||
|
|
||||||
|
# Define Subcontracted Service Items
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 1",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": finished_good.name,
|
||||||
|
"fg_item_qty": 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 1",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 150,
|
||||||
|
"fg_item": finished_good.name,
|
||||||
|
"fg_item_qty": 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create Subcontracting Order
|
||||||
|
subcontracting_order = get_subcontracting_order(
|
||||||
|
service_items=service_items,
|
||||||
|
do_not_save=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assign BOM only to the first service item
|
||||||
|
subcontracting_order.items[0].bom = bom_rm_1
|
||||||
|
subcontracting_order.save()
|
||||||
|
subcontracting_order.submit()
|
||||||
|
|
||||||
|
# Prepare Raw Material Transfer Items
|
||||||
|
raw_material_transfer_items = []
|
||||||
|
for supplied_item in subcontracting_order.supplied_items:
|
||||||
|
raw_material_transfer_items.append(
|
||||||
|
{
|
||||||
|
"item_code": supplied_item.main_item_code,
|
||||||
|
"rm_item_code": supplied_item.rm_item_code,
|
||||||
|
"qty": supplied_item.required_qty,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transfer Raw Materials to Subcontractor Warehouse
|
||||||
|
stock_entry = frappe.get_doc(
|
||||||
|
make_subcontract_transfer_entry(
|
||||||
|
subcontracting_order.name,
|
||||||
|
raw_material_transfer_items,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stock_entry.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||||
|
stock_entry.save()
|
||||||
|
stock_entry.submit()
|
||||||
|
|
||||||
|
# Create Subcontracting Receipt
|
||||||
|
subcontracting_receipt = make_subcontracting_receipt(subcontracting_order.name)
|
||||||
|
subcontracting_receipt.save()
|
||||||
|
|
||||||
|
# Check consumed_qty for each supplied item
|
||||||
|
self.assertEqual(len(subcontracting_receipt.supplied_items), 2)
|
||||||
|
self.assertEqual(subcontracting_receipt.supplied_items[0].consumed_qty, 10)
|
||||||
|
self.assertEqual(subcontracting_receipt.supplied_items[1].consumed_qty, 10)
|
||||||
|
|
||||||
def test_supplied_items_cost_after_reposting(self):
|
def test_supplied_items_cost_after_reposting(self):
|
||||||
# Set Backflush Based On as "BOM"
|
# Set Backflush Based On as "BOM"
|
||||||
set_backflush_based_on("BOM")
|
set_backflush_based_on("BOM")
|
||||||
|
|||||||
Reference in New Issue
Block a user