mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-16 13:32:13 +00:00
Merge pull request #44887 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
1
.github/helper/documentation.py
vendored
1
.github/helper/documentation.py
vendored
@@ -10,6 +10,7 @@ WEBSITE_REPOS = [
|
||||
|
||||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"docs.frappe.io",
|
||||
"frappeframework.com",
|
||||
]
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,7 +48,7 @@ class BankAccount(Document):
|
||||
self.name = self.account_name + " - " + self.bank
|
||||
|
||||
def on_trash(self):
|
||||
delete_contact_and_address("BankAccount", self.name)
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
|
||||
@@ -117,9 +117,9 @@ class BankClearance(Document):
|
||||
)
|
||||
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||
)
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@@ -117,11 +117,13 @@ class POSInvoiceMergeLog(Document):
|
||||
sales = [d for d in pos_invoice_docs if d.get("is_return") == 0]
|
||||
|
||||
sales_invoice, credit_note = "", ""
|
||||
sales_invoice_doc = None
|
||||
if sales:
|
||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||
sales_invoice_doc = self.process_merging_into_sales_invoice(sales)
|
||||
sales_invoice = sales_invoice_doc.name
|
||||
|
||||
if returns:
|
||||
credit_note = self.process_merging_into_credit_note(returns, sales_invoice)
|
||||
credit_note = self.process_merging_into_credit_note(returns, sales_invoice_doc)
|
||||
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
@@ -152,15 +154,23 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice.name
|
||||
return sales_invoice
|
||||
|
||||
def process_merging_into_credit_note(self, data, sales_invoice):
|
||||
def process_merging_into_credit_note(self, data, sales_invoice_doc=None):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||
referenes = {}
|
||||
|
||||
credit_note.return_against = sales_invoice
|
||||
if sales_invoice_doc:
|
||||
credit_note.return_against = sales_invoice_doc.name
|
||||
|
||||
for d in sales_invoice_doc.items:
|
||||
referenes[d.item_code] = d.name
|
||||
|
||||
for d in credit_note.items:
|
||||
d.sales_invoice_item = referenes.get(d.item_code)
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
@@ -366,7 +376,12 @@ class POSInvoiceMergeLog(Document):
|
||||
return []
|
||||
|
||||
def cancel_linked_invoices(self):
|
||||
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
|
||||
invoices = [self.consolidated_invoice, self.consolidated_credit_note]
|
||||
if not invoices:
|
||||
return
|
||||
|
||||
invoices.reverse()
|
||||
for si_name in invoices:
|
||||
if not si_name:
|
||||
continue
|
||||
si = frappe.get_doc("Sales Invoice", si_name)
|
||||
|
||||
@@ -1677,7 +1677,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if pi:
|
||||
pi = pi[0][0]
|
||||
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
|
||||
|
||||
frappe.throw(
|
||||
_("Supplier Invoice No exists in Purchase Invoice {0}").format(
|
||||
get_link_to_form("Purchase Invoice", pi)
|
||||
)
|
||||
)
|
||||
|
||||
def update_billing_status_in_pr(self, update_modified=True):
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
|
||||
|
||||
@@ -1838,6 +1838,52 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_adjust_incoming_rate_for_rejected_item(self):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rejected_qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.qty = 1
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "warehouse": pi.items[0].warehouse},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": pr.name,
|
||||
"warehouse": pi.items[0].rejected_warehouse,
|
||||
},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertFalse(stock_value_difference)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||
|
||||
def test_item_less_defaults(self):
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
pi.supplier = "_Test Supplier"
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "180px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -40,7 +41,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -111,13 +112,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-23 21:13:18.013816",
|
||||
"modified": "2024-12-20 12:04:46.729972",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
|
||||
@@ -16,6 +16,7 @@ class PurchaseInvoiceAdvance(Document):
|
||||
|
||||
advance_amount: DF.Currency
|
||||
allocated_amount: DF.Currency
|
||||
difference_posting_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -1002,9 +1002,9 @@ class SalesInvoice(SellingController):
|
||||
def validate_pos(self):
|
||||
if self.is_return:
|
||||
invoice_total = self.rounded_total or self.grand_total
|
||||
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > 1.0 / (
|
||||
10.0 ** (self.precision("grand_total") + 1.0)
|
||||
):
|
||||
if abs(flt(self.paid_amount)) + abs(flt(self.write_off_amount)) - abs(
|
||||
flt(invoice_total)
|
||||
) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
|
||||
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
||||
|
||||
def validate_warehouse(self):
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"advance_amount",
|
||||
"allocated_amount",
|
||||
"exchange_gain_loss",
|
||||
"ref_exchange_rate"
|
||||
"ref_exchange_rate",
|
||||
"difference_posting_date"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -30,7 +31,7 @@
|
||||
"width": "250px"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
@@ -41,7 +42,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"columns": 2,
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -112,13 +113,20 @@
|
||||
"label": "Reference Exchange Rate",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "difference_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Posting Date"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-23 21:12:57.557731",
|
||||
"modified": "2024-12-20 11:58:28.962370",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
|
||||
@@ -16,6 +16,7 @@ class SalesInvoiceAdvance(Document):
|
||||
|
||||
advance_amount: DF.Currency
|
||||
allocated_amount: DF.Currency
|
||||
difference_posting_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -247,14 +247,14 @@ def get_tax_row_for_tds(tax_details, tax_amount):
|
||||
}
|
||||
|
||||
|
||||
def get_lower_deduction_certificate(company, tax_details, pan_no):
|
||||
def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
|
||||
ldc_name = frappe.db.get_value(
|
||||
"Lower Deduction Certificate",
|
||||
{
|
||||
"pan_no": pan_no,
|
||||
"tax_withholding_category": tax_details.tax_withholding_category,
|
||||
"valid_from": (">=", tax_details.from_date),
|
||||
"valid_upto": ("<=", tax_details.to_date),
|
||||
"valid_from": ("<=", posting_date),
|
||||
"valid_upto": (">=", posting_date),
|
||||
"company": company,
|
||||
},
|
||||
"name",
|
||||
@@ -302,7 +302,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
|
||||
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
if ldc:
|
||||
@@ -539,7 +539,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
)
|
||||
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.tax_withholding_net_total
|
||||
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
if type.payment_type == "Pay":
|
||||
@@ -551,9 +551,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.base_tax_withholding_net_total
|
||||
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
|
||||
else:
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
|
||||
@@ -134,7 +134,6 @@ class ReceivablePayableReport:
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
@@ -150,6 +149,9 @@ class ReceivablePayableReport:
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -275,9 +277,6 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
|
||||
gl_entries_by_account,
|
||||
accounts_by_name,
|
||||
accounts,
|
||||
ignore_closing_entries=False,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
root_type=root_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -637,6 +637,7 @@ class GrossProfitGenerator:
|
||||
packed_item_row = row.copy()
|
||||
packed_item_row.warehouse = packed_item.warehouse
|
||||
packed_item_row.qty = packed_item.total_qty * -1
|
||||
packed_item_row.serial_and_batch_bundle = packed_item.serial_and_batch_bundle
|
||||
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
|
||||
|
||||
return flt(buying_amount, self.currency_precision)
|
||||
@@ -728,6 +729,7 @@ class GrossProfitGenerator:
|
||||
"voucher_no": row.parent,
|
||||
"allow_zero_valuation": True,
|
||||
"company": self.filters.company,
|
||||
"item_code": item_code,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -748,12 +750,13 @@ class GrossProfitGenerator:
|
||||
.inner_join(purchase_invoice)
|
||||
.on(purchase_invoice.name == purchase_invoice_item.parent)
|
||||
.select(
|
||||
purchase_invoice.name,
|
||||
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor,
|
||||
)
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.posting_date <= self.filters.to_date)
|
||||
.where(purchase_invoice_item.item_code == item_code)
|
||||
.where(purchase_invoice.is_return == 0)
|
||||
.where(purchase_invoice_item.parenttype == "Purchase Invoice")
|
||||
)
|
||||
|
||||
if row.project:
|
||||
@@ -996,6 +999,7 @@ class GrossProfitGenerator:
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"invoice": row.parent,
|
||||
"serial_and_batch_bundle": row.serial_and_batch_bundle,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1047,6 +1051,7 @@ class GrossProfitGenerator:
|
||||
pki.rate,
|
||||
(pki.rate * pki.qty).as_("base_amount"),
|
||||
pki.parent_detail_docname,
|
||||
pki.serial_and_batch_bundle,
|
||||
)
|
||||
.where(pki.docstatus == 1)
|
||||
)
|
||||
|
||||
@@ -70,10 +70,10 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry":
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
if rate:
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
|
||||
total_amount = grand_total = base_total
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
|
||||
(voucher_type, name)
|
||||
@@ -405,7 +405,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
"paid_amount_after_tax",
|
||||
"base_paid_amount",
|
||||
],
|
||||
"Journal Entry": ["total_amount"],
|
||||
"Journal Entry": ["tax_withholding_category", "total_debit"],
|
||||
}
|
||||
|
||||
entries = frappe.get_all(
|
||||
@@ -427,7 +427,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
elif doctype == "Payment Entry":
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
else:
|
||||
value = [entry.total_amount] * 3
|
||||
value = [entry.total_debit] * 3
|
||||
|
||||
net_total_map[(doctype, entry.name)] = value
|
||||
|
||||
|
||||
@@ -308,12 +308,14 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
def validate_precision(self):
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
if self.gross_purchase_amount:
|
||||
self.gross_purchase_amount = flt(self.gross_purchase_amount, float_precision)
|
||||
self.gross_purchase_amount = flt(
|
||||
self.gross_purchase_amount, self.precision("gross_purchase_amount")
|
||||
)
|
||||
|
||||
if self.opening_accumulated_depreciation:
|
||||
self.opening_accumulated_depreciation = flt(
|
||||
self.opening_accumulated_depreciation, float_precision
|
||||
self.opening_accumulated_depreciation, self.precision("opening_accumulated_depreciation")
|
||||
)
|
||||
|
||||
def validate_asset_values(self):
|
||||
@@ -487,11 +489,7 @@ class Asset(AccountsController):
|
||||
|
||||
def validate_expected_value_after_useful_life(self):
|
||||
for row in self.get("finance_books"):
|
||||
row.expected_value_after_useful_life = flt(
|
||||
row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
|
||||
)
|
||||
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
|
||||
|
||||
if not depr_schedule:
|
||||
continue
|
||||
|
||||
|
||||
@@ -889,7 +889,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
["2030-12-31", 28630.14, 28630.14],
|
||||
["2031-12-31", 35684.93, 64315.07],
|
||||
["2032-12-31", 17842.46, 82157.53],
|
||||
["2033-06-06", 5342.46, 87499.99],
|
||||
["2033-06-06", 5342.47, 87500.00],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
|
||||
@@ -344,7 +344,7 @@ class AssetDepreciationSchedule(Document):
|
||||
date_of_disposal,
|
||||
original_schedule_date=schedule_date,
|
||||
)
|
||||
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
if depreciation_amount > 0:
|
||||
self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
|
||||
|
||||
@@ -430,6 +430,7 @@ class AssetDepreciationSchedule(Document):
|
||||
|
||||
if not depreciation_amount:
|
||||
continue
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
value_after_depreciation = flt(
|
||||
value_after_depreciation - flt(depreciation_amount),
|
||||
asset_doc.precision("gross_purchase_amount"),
|
||||
@@ -443,6 +444,7 @@ class AssetDepreciationSchedule(Document):
|
||||
depreciation_amount += flt(value_after_depreciation) - flt(
|
||||
row.expected_value_after_useful_life
|
||||
)
|
||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||
skip_row = True
|
||||
|
||||
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
|
||||
@@ -517,10 +519,13 @@ class AssetDepreciationSchedule(Document):
|
||||
i - 1
|
||||
].accumulated_depreciation_amount
|
||||
else:
|
||||
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||
accumulated_depreciation = flt(
|
||||
self.opening_accumulated_depreciation,
|
||||
asset_doc.precision("opening_accumulated_depreciation"),
|
||||
)
|
||||
|
||||
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
value_after_depreciation -= flt(d.depreciation_amount)
|
||||
value_after_depreciation = flt(value_after_depreciation, d.precision("depreciation_amount"))
|
||||
|
||||
# for the last row, if depreciation method = Straight Line
|
||||
if (
|
||||
@@ -530,12 +535,11 @@ class AssetDepreciationSchedule(Document):
|
||||
and not date_of_return
|
||||
and not row.shift_based
|
||||
):
|
||||
depreciation_amount += flt(
|
||||
d.depreciation_amount += flt(
|
||||
value_after_depreciation - flt(row.expected_value_after_useful_life),
|
||||
d.precision("depreciation_amount"),
|
||||
)
|
||||
|
||||
d.depreciation_amount = depreciation_amount
|
||||
accumulated_depreciation += d.depreciation_amount
|
||||
d.accumulated_depreciation_amount = flt(
|
||||
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
|
||||
|
||||
@@ -400,11 +400,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cur_frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
this.make_subcontracting_order,
|
||||
__("Create")
|
||||
);
|
||||
if (!doc.items.every((item) => item.qty == item.sco_qty)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
me.make_subcontracting_order();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -867,27 +867,40 @@ def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_order(source_name, target_doc=None, save=False, submit=False, notify=False):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
if not is_po_fully_subcontracted(source_name):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
return target_doc
|
||||
else:
|
||||
frappe.throw(_("This PO has been fully subcontracted."))
|
||||
|
||||
|
||||
def is_po_fully_subcontracted(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.sco_qty))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
@@ -931,7 +944,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": [],
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.sco_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
@@ -939,12 +953,3 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_subcontracting_order_created(po_name) -> bool:
|
||||
return (
|
||||
True
|
||||
if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]})
|
||||
else False
|
||||
)
|
||||
|
||||
@@ -1004,7 +1004,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
)
|
||||
|
||||
def update_items(po, qty):
|
||||
trans_items = [po.items[0].as_dict()]
|
||||
trans_items = [po.items[0].as_dict().update({"docname": po.items[0].name})]
|
||||
trans_items[0]["qty"] = qty
|
||||
trans_items[0]["fg_item_qty"] = qty
|
||||
trans_items = json.dumps(trans_items, default=str)
|
||||
@@ -1059,6 +1059,73 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertEqual(po.items[0].qty, 30)
|
||||
self.assertEqual(po.items[0].fg_item_qty, 30)
|
||||
|
||||
def test_new_sc_flow(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order
|
||||
|
||||
po = create_po_for_sc_testing()
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
sco.items[0].qty = 5
|
||||
sco.items.pop(1)
|
||||
sco.items[1].qty = 25
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 1: Quantity of Service Items should change based on change in Quantity of its corresponding Finished Goods Item
|
||||
self.assertEqual(sco.service_items[0].qty, 5)
|
||||
|
||||
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].sco_qty, 5)
|
||||
self.assertEqual(po.items[1].sco_qty, 0)
|
||||
self.assertEqual(po.items[2].sco_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)
|
||||
self.assertEqual(sco.service_items[0].amount, 500)
|
||||
|
||||
# Test - 4: Service Items should be removed if its corresponding Finished Good line item is deleted
|
||||
self.assertEqual(len(sco.service_items), 2)
|
||||
|
||||
# Test - 5: Service Item quantity calculation should be based upon conversion factor calculated from its corresponding PO Item
|
||||
self.assertEqual(sco.service_items[1].qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
sco.items[0].qty = 6
|
||||
|
||||
# Test - 6: Saving document should not be allowed if Quantity exceeds available Subcontracting Quantity of any Purchase Order Item
|
||||
self.assertRaises(frappe.ValidationError, sco.save)
|
||||
|
||||
sco.items[0].qty = 5
|
||||
sco.items.pop()
|
||||
sco.items.pop()
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
# Test - 7: Since line item 1 is now fully subcontracted, new SCO should by default only have the remaining 2 line items
|
||||
self.assertEqual(len(sco.items), 2)
|
||||
|
||||
sco.items.pop(0)
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 25)
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 8: Since this PO is now fully subcontracted, creating a new SCO from it should throw error
|
||||
self.assertRaises(frappe.ValidationError, make_subcontracting_order, po.name)
|
||||
|
||||
@change_settings("Buying Settings", {"auto_create_subcontracting_order": 1})
|
||||
def test_auto_create_subcontracting_order(self):
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
@@ -1124,6 +1191,53 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertEqual(po.per_billed, 100)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
make_bom_for_subcontracted_items,
|
||||
make_raw_materials,
|
||||
make_service_items,
|
||||
make_subcontracted_items,
|
||||
)
|
||||
|
||||
make_subcontracted_items()
|
||||
make_raw_materials()
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA1",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 2",
|
||||
"qty": 20,
|
||||
"rate": 25,
|
||||
"fg_item": "Subcontracted Item SA2",
|
||||
"fg_item_qty": 15,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 3",
|
||||
"qty": 25,
|
||||
"rate": 10,
|
||||
"fg_item": "Subcontracted Item SA3",
|
||||
"fg_item_qty": 50,
|
||||
},
|
||||
]
|
||||
|
||||
return create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-05-24 19:29:06",
|
||||
"creation": "2024-12-09 12:54:24.652161",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
@@ -26,6 +26,7 @@
|
||||
"quantity_and_rate",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"sco_qty",
|
||||
"col_break2",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
@@ -909,13 +910,21 @@
|
||||
{
|
||||
"fieldname": "column_break_fyqr",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "sco_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-05 11:23:24.859435",
|
||||
"modified": "2024-12-10 12:11:18.536089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -80,6 +80,7 @@ class PurchaseOrderItem(Document):
|
||||
sales_order_item: DF.Data | None
|
||||
sales_order_packed_item: DF.Data | None
|
||||
schedule_date: DF.Date
|
||||
sco_qty: DF.Float
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
stock_uom_rate: DF.Currency
|
||||
|
||||
@@ -379,13 +379,14 @@ class AccountsController(TransactionBase):
|
||||
== 1
|
||||
)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
|
||||
)
|
||||
frappe.db.sql(
|
||||
"delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s",
|
||||
(self.doctype, self.name),
|
||||
)
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
frappe.qb.from_(gle).delete().where(
|
||||
(gle.voucher_type == self.doctype) & (gle.voucher_no == self.name)
|
||||
).run()
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
frappe.qb.from_(sle).delete().where(
|
||||
(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
|
||||
).run()
|
||||
|
||||
def remove_serial_and_batch_bundle(self):
|
||||
bundles = frappe.get_all(
|
||||
@@ -1150,11 +1151,12 @@ class AccountsController(TransactionBase):
|
||||
def clear_unallocated_advances(self, childtype, parentfield):
|
||||
self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]}))
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tab{}` where parentfield={} and parent = {}
|
||||
and allocated_amount = 0""".format(childtype, "%s", "%s"),
|
||||
(parentfield, self.name),
|
||||
)
|
||||
doctype = frappe.qb.DocType(childtype)
|
||||
frappe.qb.from_(doctype).delete().where(
|
||||
(doctype.parentfield == parentfield)
|
||||
& (doctype.parent == self.name)
|
||||
& (doctype.allocated_amount == 0)
|
||||
).run()
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_shipping_rule(self):
|
||||
@@ -1204,6 +1206,7 @@ class AccountsController(TransactionBase):
|
||||
"advance_amount": flt(d.amount),
|
||||
"allocated_amount": allocated_amount,
|
||||
"ref_exchange_rate": flt(d.exchange_rate), # exchange_rate of advance entry
|
||||
"difference_posting_date": self.posting_date,
|
||||
}
|
||||
if d.get("paid_from"):
|
||||
advance_row["account"] = d.paid_from
|
||||
@@ -1214,6 +1217,8 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def get_advance_entries(self, include_unallocated=True):
|
||||
party_account = []
|
||||
default_advance_account = None
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
party_type = "Customer"
|
||||
party = self.customer
|
||||
@@ -1229,10 +1234,14 @@ class AccountsController(TransactionBase):
|
||||
order_doctype = "Purchase Order"
|
||||
party_account.append(self.credit_to)
|
||||
|
||||
party_account.extend(
|
||||
get_party_account(party_type, party=party, company=self.company, include_advance=True)
|
||||
party_accounts = get_party_account(
|
||||
party_type, party=party, company=self.company, include_advance=True
|
||||
)
|
||||
|
||||
if party_accounts:
|
||||
party_account.append(party_accounts[0])
|
||||
default_advance_account = party_accounts[1] if len(party_accounts) == 2 else None
|
||||
|
||||
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
journal_entries = get_advance_journal_entries(
|
||||
@@ -1240,7 +1249,13 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
party_type, party, party_account, order_doctype, order_list, include_unallocated
|
||||
party_type,
|
||||
party,
|
||||
party_account,
|
||||
order_doctype,
|
||||
order_list,
|
||||
default_advance_account,
|
||||
include_unallocated,
|
||||
)
|
||||
|
||||
res = journal_entries + payment_entries
|
||||
@@ -1497,7 +1512,6 @@ class AccountsController(TransactionBase):
|
||||
gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
)
|
||||
|
||||
je = create_gain_loss_journal(
|
||||
self.company,
|
||||
args.get("difference_posting_date") if args else self.posting_date,
|
||||
@@ -1583,6 +1597,7 @@ class AccountsController(TransactionBase):
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
),
|
||||
"exchange_gain_loss": flt(d.get("exchange_gain_loss")),
|
||||
"difference_posting_date": d.get("difference_posting_date"),
|
||||
}
|
||||
)
|
||||
lst.append(args)
|
||||
@@ -2116,11 +2131,9 @@ class AccountsController(TransactionBase):
|
||||
for adv in self.advances:
|
||||
consider_for_total_advance = True
|
||||
if adv.reference_name == linked_doc_name:
|
||||
frappe.db.sql(
|
||||
f"""delete from `tab{self.doctype} Advance`
|
||||
where name = %s""",
|
||||
adv.name,
|
||||
)
|
||||
doctype = frappe.qb.DocType(self.doctype + " Advance")
|
||||
frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run()
|
||||
|
||||
consider_for_total_advance = False
|
||||
|
||||
if consider_for_total_advance:
|
||||
|
||||
@@ -156,7 +156,7 @@ class StockController(AccountsController):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
is_material_issue = False
|
||||
if self.doctype == "Stock Entry" and self.purpose == "Material Issue":
|
||||
if self.doctype == "Stock Entry" and self.purpose in ["Material Issue", "Material Transfer"]:
|
||||
is_material_issue = True
|
||||
|
||||
for d in self.get("items"):
|
||||
@@ -530,7 +530,7 @@ class StockController(AccountsController):
|
||||
"account": warehouse_account[sle.warehouse]["account"],
|
||||
"against": expense_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.project or self.get("project"),
|
||||
"project": sle.get("project") or item_row.project or self.get("project"),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": flt(sle.stock_value_difference, precision),
|
||||
"is_opening": item_row.get("is_opening")
|
||||
@@ -550,7 +550,9 @@ class StockController(AccountsController):
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": -1 * flt(sle.stock_value_difference, precision),
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"project": sle.get("project")
|
||||
or item_row.get("project")
|
||||
or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening")
|
||||
or self.get("is_opening")
|
||||
or "No",
|
||||
@@ -678,23 +680,34 @@ class StockController(AccountsController):
|
||||
|
||||
def get_stock_ledger_details(self):
|
||||
stock_ledger = {}
|
||||
stock_ledger_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, warehouse, stock_value_difference, valuation_rate,
|
||||
voucher_detail_no, item_code, posting_date, posting_time,
|
||||
actual_qty, qty_after_transaction
|
||||
from
|
||||
`tabStock Ledger Entry`
|
||||
where
|
||||
voucher_type=%s and voucher_no=%s and is_cancelled = 0
|
||||
""",
|
||||
(self.doctype, self.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
stock_ledger_entries = (
|
||||
frappe.qb.from_(table)
|
||||
.select(
|
||||
table.name,
|
||||
table.warehouse,
|
||||
table.stock_value_difference,
|
||||
table.valuation_rate,
|
||||
table.voucher_detail_no,
|
||||
table.item_code,
|
||||
table.posting_date,
|
||||
table.posting_time,
|
||||
table.actual_qty,
|
||||
table.qty_after_transaction,
|
||||
table.project,
|
||||
)
|
||||
.where(
|
||||
(table.voucher_type == self.doctype)
|
||||
& (table.voucher_no == self.name)
|
||||
& (table.is_cancelled == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
for sle in stock_ledger_entries:
|
||||
stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
|
||||
|
||||
return stock_ledger
|
||||
|
||||
def check_expense_account(self, item):
|
||||
@@ -999,6 +1012,9 @@ class StockController(AccountsController):
|
||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||
qi_required = True # inward stock needs inspection
|
||||
|
||||
if row.get("is_scrap_item"):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
self.validate_qi_presence(row)
|
||||
if self.docstatus == 1:
|
||||
@@ -1758,6 +1774,9 @@ def make_bundle_for_material_transfer(**kwargs):
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
if kwargs.do_not_submit:
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
else:
|
||||
bundle_doc.submit()
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -103,6 +103,19 @@ class SubcontractingController(StockController):
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
)
|
||||
|
||||
if (
|
||||
self.doctype not in "Subcontracting Receipt"
|
||||
and item.qty
|
||||
> flt(get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item))
|
||||
/ item.sc_conversion_factor
|
||||
):
|
||||
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 item.bom:
|
||||
is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
|
||||
|
||||
@@ -1110,6 +1123,12 @@ def get_item_details(items):
|
||||
return item_details
|
||||
|
||||
|
||||
def get_pending_sco_qty(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name)
|
||||
return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(
|
||||
subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -1979,3 +1981,95 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertEqual(len(exc_je_for_adv), 0)
|
||||
|
||||
self.remove_advance_accounts_from_party_master()
|
||||
|
||||
def test_difference_posting_date_in_pi_and_si(self):
|
||||
self.setup_advance_accounts_in_party_master()
|
||||
|
||||
# create payment entry for customer
|
||||
adv = self.create_payment_entry(amount=1, source_exc_rate=83)
|
||||
adv.save()
|
||||
self.assertEqual(adv.paid_from, self.advance_received_usd)
|
||||
adv.submit()
|
||||
adv.reload()
|
||||
|
||||
# create sales invoice with advance received
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True)
|
||||
si.debit_to = self.debtors_usd
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": adv.doctype,
|
||||
"reference_name": adv.name,
|
||||
"remarks": "Amount INR 1 received from _Test MC Customer USD\nTransaction reference no Test001 dated 2024-12-19",
|
||||
"advance_amount": 1.0,
|
||||
"allocated_amount": 1.0,
|
||||
"exchange_gain_loss": 3.0,
|
||||
"ref_exchange_rate": 83.0,
|
||||
"difference_posting_date": add_days(nowdate(), -2),
|
||||
},
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
# exc Gain/Loss journal should've been creatad
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_adv), 1)
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_adv)
|
||||
|
||||
# check jv created with difference_posting_date in sales invoice
|
||||
jv = frappe.get_doc("Journal Entry", exc_je_for_si[0].parent)
|
||||
sales_invoice = frappe.get_doc("Sales Invoice", si.name)
|
||||
self.assertEqual(sales_invoice.advances[0].difference_posting_date, jv.posting_date)
|
||||
|
||||
# create payment entry for supplier
|
||||
usd_amount = 1
|
||||
inr_amount = 85
|
||||
exc_rate = 85
|
||||
adv = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Pay",
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
paid_from=self.cash,
|
||||
paid_to=self.advance_paid_usd,
|
||||
paid_amount=inr_amount,
|
||||
)
|
||||
adv.source_exchange_rate = 1
|
||||
adv.target_exchange_rate = exc_rate
|
||||
adv.received_amount = usd_amount
|
||||
adv.paid_amount = exc_rate * usd_amount
|
||||
adv.posting_date = nowdate()
|
||||
adv.save()
|
||||
self.assertEqual(adv.paid_to, self.advance_paid_usd)
|
||||
adv.submit()
|
||||
|
||||
# create purchase invoice with advance paid
|
||||
pi = self.create_purchase_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True)
|
||||
pi.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": adv.doctype,
|
||||
"reference_name": adv.name,
|
||||
"remarks": "Amount INR 1 paid to _Test MC Supplier USD\nTransaction reference no Test001 dated 2024-12-20",
|
||||
"advance_amount": 1.0,
|
||||
"allocated_amount": 1.0,
|
||||
"exchange_gain_loss": 5.0,
|
||||
"ref_exchange_rate": 85.0,
|
||||
"difference_posting_date": add_days(nowdate(), -2),
|
||||
},
|
||||
)
|
||||
pi.save().submit()
|
||||
self.assertEqual(pi.credit_to, self.creditors_usd)
|
||||
|
||||
# exc Gain/Loss journal should've been creatad
|
||||
exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(len(exc_je_for_pi), 1)
|
||||
self.assertEqual(len(exc_je_for_adv), 1)
|
||||
self.assertEqual(exc_je_for_pi, exc_je_for_adv)
|
||||
|
||||
# check jv created with difference_posting_date in purchase invoice
|
||||
journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent)
|
||||
purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name)
|
||||
self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date)
|
||||
|
||||
@@ -1261,6 +1261,7 @@ def make_raw_materials():
|
||||
for item, properties in raw_materials.items():
|
||||
if not frappe.db.exists("Item", item):
|
||||
properties.update({"is_stock_item": 1})
|
||||
properties.update({"valuation_rate": 100})
|
||||
make_item(item, properties)
|
||||
|
||||
|
||||
@@ -1311,7 +1312,7 @@ def make_bom_for_subcontracted_items():
|
||||
|
||||
for item_code, raw_materials in boms.items():
|
||||
if not frappe.db.exists("BOM", {"item": item_code}):
|
||||
make_bom(item=item_code, raw_materials=raw_materials, rate=100)
|
||||
make_bom(item=item_code, raw_materials=raw_materials, rate=100, currency="INR")
|
||||
|
||||
|
||||
def set_backflush_based_on(based_on):
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"table_fieldname": "competitors"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:33:54.284279",
|
||||
"modified": "2024-12-10 08:26:38.496003",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Competitor",
|
||||
@@ -53,20 +53,25 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance User"
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -25,7 +25,12 @@ frappe.ui.form.on("Plaid Settings", {
|
||||
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
|
||||
let bank_transaction_link = frappe.utils.get_form_link(
|
||||
"Bank Transaction",
|
||||
"",
|
||||
true,
|
||||
"Bank Transaction"
|
||||
);
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Sync Started"),
|
||||
|
||||
@@ -944,8 +944,9 @@ class JobCard(Document):
|
||||
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
|
||||
min_qty = []
|
||||
for d in doc.operations:
|
||||
if d.completed_qty:
|
||||
min_qty.append(d.completed_qty)
|
||||
completed_qty = flt(d.completed_qty) + flt(d.process_loss_qty)
|
||||
if completed_qty:
|
||||
min_qty.append(completed_qty)
|
||||
else:
|
||||
min_qty = []
|
||||
break
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings, timeout
|
||||
from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, today
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import make_stock_entry as make_stock_entry_from_jc
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
CapacityError,
|
||||
@@ -505,6 +506,60 @@ class TestWorkOrder(FrappeTestCase):
|
||||
for stock_entry in stock_entries:
|
||||
stock_entry.cancel()
|
||||
|
||||
def test_work_order_material_transferred_qty_with_process_loss(self):
|
||||
stock_entries = []
|
||||
bom = frappe.get_doc("BOM", {"docstatus": 1, "with_operations": 1, "company": "_Test Company"})
|
||||
|
||||
work_order = make_wo_order_test_record(
|
||||
item=bom.item,
|
||||
qty=2,
|
||||
bom_no=bom.name,
|
||||
source_warehouse="_Test Warehouse - _TC",
|
||||
transfer_material_against="Job Card",
|
||||
)
|
||||
|
||||
self.assertEqual(work_order.qty, 2)
|
||||
|
||||
for row in work_order.required_items:
|
||||
stock_entry_doc = test_stock_entry.make_stock_entry(
|
||||
item_code=row.item_code, target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100
|
||||
)
|
||||
stock_entries.append(stock_entry_doc)
|
||||
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card", filters={"work_order": work_order.name}, order_by="creation asc"
|
||||
)
|
||||
|
||||
for row in job_cards:
|
||||
transfer_entry_1 = make_stock_entry_from_jc(row.name)
|
||||
transfer_entry_1.submit()
|
||||
|
||||
doc = frappe.get_doc("Job Card", row.name)
|
||||
for row in doc.scheduled_time_logs:
|
||||
doc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": row.from_time,
|
||||
"to_time": row.to_time,
|
||||
"time_in_mins": row.time_in_mins,
|
||||
"completed_qty": 1,
|
||||
},
|
||||
)
|
||||
|
||||
doc.save()
|
||||
doc.submit()
|
||||
|
||||
self.assertEqual(doc.total_completed_qty, 1)
|
||||
self.assertEqual(doc.process_loss_qty, 1)
|
||||
|
||||
work_order.reload()
|
||||
|
||||
self.assertEqual(work_order.material_transferred_for_manufacturing, 2)
|
||||
|
||||
for row in work_order.operations:
|
||||
self.assertEqual(row.completed_qty, 1)
|
||||
self.assertEqual(row.process_loss_qty, 1)
|
||||
|
||||
def test_capcity_planning(self):
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings", {"disable_capacity_planning": 0, "capacity_planning_for_days": 1}
|
||||
|
||||
@@ -574,11 +574,19 @@ erpnext.PointOfSale.Controller = class {
|
||||
} else {
|
||||
if (!this.frm.doc.customer) return this.raise_customer_selection_alert();
|
||||
|
||||
const { item_code, batch_no, serial_no, rate, uom } = item;
|
||||
const { item_code, batch_no, serial_no, rate, uom, stock_uom } = item;
|
||||
|
||||
if (!item_code) return;
|
||||
|
||||
const new_item = { item_code, batch_no, rate, uom, [field]: value };
|
||||
if (rate == undefined || rate == 0) {
|
||||
frappe.show_alert({
|
||||
message: __("Price is not set for the item."),
|
||||
indicator: "orange",
|
||||
});
|
||||
frappe.utils.play_sound("error");
|
||||
return;
|
||||
}
|
||||
const new_item = { item_code, batch_no, rate, uom, [field]: value, stock_uom };
|
||||
|
||||
if (serial_no) {
|
||||
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
|
||||
|
||||
@@ -118,6 +118,7 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
|
||||
data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
|
||||
data-rate="${escape(price_list_rate || 0)}"
|
||||
data-stock-uom="${escape(item.stock_uom)}"
|
||||
title="${item.item_name}">
|
||||
|
||||
${get_item_image_html()}
|
||||
@@ -251,17 +252,19 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
let serial_no = unescape($item.attr("data-serial-no"));
|
||||
let uom = unescape($item.attr("data-uom"));
|
||||
let rate = unescape($item.attr("data-rate"));
|
||||
let stock_uom = unescape($item.attr("data-stock-uom"));
|
||||
|
||||
// escape(undefined) returns "undefined" then unescape returns "undefined"
|
||||
batch_no = batch_no === "undefined" ? undefined : batch_no;
|
||||
serial_no = serial_no === "undefined" ? undefined : serial_no;
|
||||
uom = uom === "undefined" ? undefined : uom;
|
||||
rate = rate === "undefined" ? undefined : rate;
|
||||
stock_uom = stock_uom === "undefined" ? undefined : stock_uom;
|
||||
|
||||
me.events.item_selected({
|
||||
field: "qty",
|
||||
value: "+1",
|
||||
item: { item_code, batch_no, serial_no, uom, rate },
|
||||
item: { item_code, batch_no, serial_no, uom, rate, stock_uom },
|
||||
});
|
||||
me.search_field.set_focus();
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"table_fieldname": "lost_reasons"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:31:02.743353",
|
||||
"modified": "2024-12-10 08:21:38.280627",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Quotation Lost Reason",
|
||||
@@ -49,6 +49,22 @@
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance User"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Maintenance Manager"
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
|
||||
@@ -4500,9 +4500,200 @@
|
||||
},
|
||||
|
||||
"Sweden": {
|
||||
"Sweden Tax": {
|
||||
"account_name": "VAT",
|
||||
"tax_rate": 25.00
|
||||
"tax_categories": [],
|
||||
"chart_of_accounts": {
|
||||
"*": {
|
||||
"sales_tax_templates": [
|
||||
{
|
||||
"title": "Försäljning Moms 25%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Utgående moms, 25 %",
|
||||
"account_number": "2610",
|
||||
"tax_rate": 25.00
|
||||
},
|
||||
"description": "Moms 25%",
|
||||
"rate": 25.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Försäljning Moms 12%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Utgående moms, 12 %",
|
||||
"account_number": "2620",
|
||||
"tax_rate": 12.00
|
||||
},
|
||||
"description": "Moms 12%",
|
||||
"rate": 12.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Försäljning Moms 6%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Utgående moms, 6 %",
|
||||
"account_number": "2630",
|
||||
"tax_rate": 6.00
|
||||
},
|
||||
"description": "Moms 6%",
|
||||
"rate": 6.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Försäljning Moms 0%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Utgående moms, 6 %",
|
||||
"account_number": "2630",
|
||||
"tax_rate": 0.00
|
||||
},
|
||||
"description": "Moms 0%",
|
||||
"rate": 0.00
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"purchase_tax_templates": [
|
||||
{
|
||||
"title": "Inköp Moms 25%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Ingående moms",
|
||||
"account_number": "2640",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 25.00
|
||||
},
|
||||
"description": "Moms 25%",
|
||||
"rate": 25.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Inköp Moms 12%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Ingående moms",
|
||||
"account_number": "2640",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 12.00
|
||||
},
|
||||
"description": "Moms 12%",
|
||||
"rate": 12.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Inköp Moms 6%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Ingående moms",
|
||||
"account_number": "2640",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 6.00
|
||||
},
|
||||
"description": "Moms 6%",
|
||||
"rate": 6.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Inköp Moms 0%",
|
||||
"is_default": 0,
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Ingående moms",
|
||||
"account_number": "2640",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 0.00
|
||||
},
|
||||
"description": "Moms 0%",
|
||||
"rate": 0.00
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"item_tax_templates": [
|
||||
{
|
||||
"title": "Artikel Moms 25%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Utgående moms, 25 %",
|
||||
"account_number": "2610",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 25.00
|
||||
},
|
||||
"description": "Moms 25%",
|
||||
"tax_rate": 25.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Artikel Moms 12%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Utgående moms, 12 %",
|
||||
"account_number": "2620",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 12.00
|
||||
},
|
||||
"description": "Moms 12%",
|
||||
"tax_rate": 12.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Artikel Moms 6%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Utgående moms, 6 %",
|
||||
"account_number": "2630",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 6.00
|
||||
},
|
||||
"description": "Moms 6%",
|
||||
"tax_rate": 6.00
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Artikel Moms 0%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "Utgående moms, 0 %",
|
||||
"account_number": "2611",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 0.00
|
||||
},
|
||||
"description": "Moms 0%",
|
||||
"tax_rate": 0.00
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
{
|
||||
"chart_name": "Warehouse wise Stock Value",
|
||||
"chart_type": "Custom",
|
||||
"creation": "2020-07-20 21:01:04.296157",
|
||||
"creation": "2022-03-30 00:58:02.018824",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"filters_json": "{}",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-07-22 13:01:01.815123",
|
||||
"last_synced_on": "2024-12-23 18:44:46.822164",
|
||||
"modified": "2024-12-23 19:31:17.003946",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse wise Stock Value",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"source": "Warehouse wise Stock Value",
|
||||
"timeseries": 0,
|
||||
"type": "Bar",
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import flt, nowtime
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from pypika import Order
|
||||
|
||||
@@ -112,7 +112,10 @@ class DeprecatedBatchNoValuation:
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
timestamp_condition = None
|
||||
if self.sle.posting_date and self.sle.posting_time:
|
||||
if self.sle.posting_date:
|
||||
if self.sle.posting_time is None:
|
||||
self.sle.posting_time = nowtime()
|
||||
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
if not self.sle.creation:
|
||||
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-18 11:46:04.448220",
|
||||
"modified": "2024-12-19 13:48:46.618066",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Closing Stock Balance",
|
||||
@@ -121,6 +121,7 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
@@ -130,10 +131,39 @@
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1247,6 +1247,7 @@ def create_stock_entry(pick_list):
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.pick_list = pick_list.get("name")
|
||||
stock_entry.purpose = pick_list.get("purpose")
|
||||
stock_entry.company = pick_list.get("company")
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
if pick_list.get("work_order"):
|
||||
|
||||
@@ -920,12 +920,17 @@ class PurchaseReceipt(BuyingController):
|
||||
)
|
||||
|
||||
def enable_recalculate_rate_in_sles(self):
|
||||
rejected_warehouses = frappe.get_all(
|
||||
"Purchase Receipt Item", filters={"parent": self.name}, pluck="rejected_warehouse"
|
||||
)
|
||||
|
||||
sle_table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
(
|
||||
frappe.qb.update(sle_table)
|
||||
.set(sle_table.recalculate_rate, 1)
|
||||
.where(sle_table.voucher_no == self.name)
|
||||
.where(sle_table.voucher_type == "Purchase Receipt")
|
||||
.where(sle_table.warehouse.notin(rejected_warehouses))
|
||||
).run()
|
||||
|
||||
|
||||
|
||||
@@ -90,12 +90,10 @@ class SerialandBatchBundle(Document):
|
||||
self.validate_duplicate_serial_and_batch_no()
|
||||
self.validate_voucher_no()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.allow_existing_serial_nos()
|
||||
|
||||
if self.type_of_transaction == "Maintenance":
|
||||
return
|
||||
|
||||
self.allow_existing_serial_nos()
|
||||
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
|
||||
self.validate_serial_nos_duplicate()
|
||||
self.check_future_entries_exists()
|
||||
@@ -809,7 +807,7 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
for serial_no, batch_no in serial_batches.items():
|
||||
if correct_batches.get(serial_no) != batch_no:
|
||||
if correct_batches.get(serial_no) and correct_batches.get(serial_no) != batch_no:
|
||||
self.throw_error_message(
|
||||
f"Serial No {bold(serial_no)} does not belong to Batch No {bold(batch_no)}"
|
||||
)
|
||||
@@ -1188,19 +1186,19 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
continue
|
||||
|
||||
if has_serial_no or (has_serial_no and has_batch_no):
|
||||
_dict = {"serial_no": row[0], "qty": 1}
|
||||
_dict = {"serial_no": row[0].strip(), "qty": 1}
|
||||
|
||||
if has_batch_no:
|
||||
_dict.update(
|
||||
{
|
||||
"batch_no": row[1],
|
||||
"batch_no": row[1].strip(),
|
||||
"qty": row[2],
|
||||
}
|
||||
)
|
||||
|
||||
batch_nos.append(
|
||||
{
|
||||
"batch_no": row[1],
|
||||
"batch_no": row[1].strip(),
|
||||
"qty": row[2],
|
||||
}
|
||||
)
|
||||
@@ -1209,7 +1207,7 @@ def parse_csv_file_to_get_serial_batch(reader):
|
||||
elif has_batch_no:
|
||||
batch_nos.append(
|
||||
{
|
||||
"batch_no": row[0],
|
||||
"batch_no": row[0].strip(),
|
||||
"qty": row[1],
|
||||
}
|
||||
)
|
||||
@@ -1253,7 +1251,7 @@ def make_serial_nos(item_code, serial_nos):
|
||||
"Item", item_code, ["description", "item_code", "item_name", "warranty_period"], as_dict=1
|
||||
)
|
||||
|
||||
serial_nos = [d.get("serial_no") for d in serial_nos if d.get("serial_no")]
|
||||
serial_nos = [d.get("serial_no").strip() for d in serial_nos if d.get("serial_no")]
|
||||
existing_serial_nos = frappe.get_all("Serial No", filters={"name": ("in", serial_nos)})
|
||||
|
||||
existing_serial_nos = [d.get("name") for d in existing_serial_nos if d.get("name")]
|
||||
@@ -2101,6 +2099,8 @@ def update_available_batches(available_batches, *reserved_batches) -> None:
|
||||
|
||||
|
||||
def get_available_batches(kwargs):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
|
||||
batch_table = frappe.qb.DocType("Batch")
|
||||
@@ -2128,9 +2128,9 @@ def get_available_batches(kwargs):
|
||||
if kwargs.get("posting_time") is None:
|
||||
kwargs.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = CombineDatetime(
|
||||
stock_ledger_entry.posting_date, stock_ledger_entry.posting_time
|
||||
) <= CombineDatetime(kwargs.posting_date, kwargs.posting_time)
|
||||
timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime(
|
||||
kwargs.posting_date, kwargs.posting_time
|
||||
)
|
||||
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
}
|
||||
)
|
||||
|
||||
doc.set_posting_datetime()
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_links = True
|
||||
@@ -586,6 +587,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
"company": "_Test Company",
|
||||
}
|
||||
)
|
||||
doc.set_posting_datetime()
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_links = True
|
||||
|
||||
@@ -371,6 +371,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
function () {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.stock_entry.stock_entry.get_expired_batch_items",
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
frm.set_value("items", []);
|
||||
|
||||
@@ -2975,17 +2975,45 @@ def get_uom_details(item_code, uom, qty):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_expired_batch_items():
|
||||
return frappe.db.sql(
|
||||
"""select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\
|
||||
from `tabBatch` b, `tabStock Ledger Entry` sle
|
||||
where b.expiry_date <= %s
|
||||
and b.expiry_date is not NULL
|
||||
and b.batch_id = sle.batch_no and sle.is_cancelled = 0
|
||||
group by sle.warehouse, sle.item_code, sle.batch_no""",
|
||||
(nowdate()),
|
||||
as_dict=1,
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_auto_batch_nos
|
||||
|
||||
expired_batches = get_expired_batches()
|
||||
if not expired_batches:
|
||||
return []
|
||||
|
||||
expired_batches_stock = get_auto_batch_nos(
|
||||
frappe._dict(
|
||||
{
|
||||
"batch_no": list(expired_batches.keys()),
|
||||
"for_stock_levels": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for row in expired_batches_stock:
|
||||
row.update(expired_batches.get(row.batch_no))
|
||||
|
||||
return expired_batches_stock
|
||||
|
||||
|
||||
def get_expired_batches():
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
data = (
|
||||
frappe.qb.from_(batch)
|
||||
.select(batch.item, batch.name.as_("batch_no"), batch.stock_uom)
|
||||
.where((batch.expiry_date <= nowdate()) & (batch.expiry_date.isnotnull()))
|
||||
).run(as_dict=True)
|
||||
|
||||
if not data:
|
||||
return []
|
||||
|
||||
expired_batches = frappe._dict()
|
||||
for row in data:
|
||||
expired_batches[row.batch_no] = row
|
||||
|
||||
return expired_batches
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_warehouse_details(args):
|
||||
|
||||
@@ -970,6 +970,80 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, ste.submit)
|
||||
|
||||
def test_quality_check_for_scrap_item(self):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as _make_stock_entry,
|
||||
)
|
||||
|
||||
scrap_item = "_Test Scrap Item 1"
|
||||
make_item(scrap_item, {"is_stock_item": 1, "is_purchase_item": 0})
|
||||
|
||||
bom_name = frappe.db.get_value("BOM Scrap Item", {"docstatus": 1}, "parent")
|
||||
production_item = frappe.db.get_value("BOM", bom_name, "item")
|
||||
|
||||
work_order = frappe.new_doc("Work Order")
|
||||
work_order.production_item = production_item
|
||||
work_order.update(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"fg_warehouse": "_Test Warehouse 1 - _TC",
|
||||
"production_item": production_item,
|
||||
"bom_no": bom_name,
|
||||
"qty": 1.0,
|
||||
"stock_uom": frappe.db.get_value("Item", production_item, "stock_uom"),
|
||||
"skip_transfer": 1,
|
||||
}
|
||||
)
|
||||
|
||||
work_order.get_items_and_operations_from_bom()
|
||||
work_order.submit()
|
||||
|
||||
stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
|
||||
for row in stock_entry.items:
|
||||
if row.s_warehouse:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target=row.s_warehouse,
|
||||
qty=row.qty,
|
||||
basic_rate=row.basic_rate or 100,
|
||||
)
|
||||
|
||||
if row.is_scrap_item:
|
||||
row.item_code = scrap_item
|
||||
row.uom = frappe.db.get_value("Item", scrap_item, "stock_uom")
|
||||
row.stock_uom = frappe.db.get_value("Item", scrap_item, "stock_uom")
|
||||
|
||||
stock_entry.inspection_required = 1
|
||||
stock_entry.save()
|
||||
|
||||
self.assertTrue([row.item_code for row in stock_entry.items if row.is_scrap_item])
|
||||
|
||||
for row in stock_entry.items:
|
||||
if not row.is_scrap_item:
|
||||
qc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Quality Inspection",
|
||||
"reference_name": stock_entry.name,
|
||||
"inspected_by": "Administrator",
|
||||
"reference_type": "Stock Entry",
|
||||
"inspection_type": "In Process",
|
||||
"status": "Accepted",
|
||||
"sample_size": 1,
|
||||
"item_code": row.item_code,
|
||||
}
|
||||
)
|
||||
|
||||
qc_name = qc.submit()
|
||||
row.quality_inspection = qc_name
|
||||
|
||||
stock_entry.reload()
|
||||
stock_entry.submit()
|
||||
for row in stock_entry.items:
|
||||
if row.is_scrap_item:
|
||||
self.assertFalse(row.quality_inspection)
|
||||
else:
|
||||
self.assertTrue(row.quality_inspection)
|
||||
|
||||
def test_quality_check(self):
|
||||
item_code = "_Test Item For QC"
|
||||
if not frappe.db.exists("Item", item_code):
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
"options": "DocType",
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
@@ -362,7 +363,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-27 09:28:03.961443",
|
||||
"modified": "2024-12-23 18:03:05.171023",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Ledger Entry",
|
||||
|
||||
@@ -88,6 +88,7 @@ class StockLedgerEntry(Document):
|
||||
self.flags.ignore_submit_comment = True
|
||||
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
|
||||
|
||||
self.set_posting_datetime()
|
||||
self.validate_mandatory()
|
||||
self.validate_batch()
|
||||
validate_disabled_warehouse(self.warehouse)
|
||||
@@ -98,15 +99,10 @@ class StockLedgerEntry(Document):
|
||||
self.validate_with_last_transaction_posting_time()
|
||||
self.validate_inventory_dimension_negative_stock()
|
||||
|
||||
def set_posting_datetime(self, save=False):
|
||||
def set_posting_datetime(self):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
if save:
|
||||
posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
|
||||
if not self.posting_datetime or self.posting_datetime != posting_datetime:
|
||||
self.db_set("posting_datetime", posting_datetime)
|
||||
else:
|
||||
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
|
||||
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
|
||||
|
||||
def validate_inventory_dimension_negative_stock(self):
|
||||
if self.is_cancelled or self.actual_qty >= 0:
|
||||
@@ -173,7 +169,6 @@ class StockLedgerEntry(Document):
|
||||
return inv_dimension_dict
|
||||
|
||||
def on_submit(self):
|
||||
self.set_posting_datetime(save=True)
|
||||
self.check_stock_frozen_date()
|
||||
|
||||
# Added to handle few test cases where serial_and_batch_bundles are not required
|
||||
|
||||
@@ -1242,6 +1242,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
}
|
||||
)
|
||||
|
||||
doc.set_posting_datetime()
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.flags.ignore_links = True
|
||||
|
||||
@@ -643,7 +643,10 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
timestamp_condition = ""
|
||||
if self.sle.posting_date and self.sle.posting_time:
|
||||
if self.sle.posting_date:
|
||||
if self.sle.posting_time is None:
|
||||
self.sle.posting_time = nowtime()
|
||||
|
||||
timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime(
|
||||
self.sle.posting_date, self.sle.posting_time
|
||||
)
|
||||
@@ -725,15 +728,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
|
||||
self.stock_value_change += stock_value_change
|
||||
|
||||
frappe.db.set_value(
|
||||
"Serial and Batch Entry",
|
||||
ledger.name,
|
||||
{
|
||||
"stock_value_difference": stock_value_change,
|
||||
"incoming_rate": self.batch_avg_rate[batch_no],
|
||||
},
|
||||
)
|
||||
|
||||
def calculate_valuation_rate(self):
|
||||
if not hasattr(self, "wh_data"):
|
||||
return
|
||||
@@ -947,12 +941,13 @@ class SerialBatchCreation:
|
||||
if self.get("make_bundle_from_sle") and self.type_of_transaction == "Inward":
|
||||
doc.flags.ignore_validate_serial_batch = True
|
||||
|
||||
doc.save()
|
||||
self.validate_qty(doc)
|
||||
|
||||
if not hasattr(self, "do_not_submit") or not self.do_not_submit:
|
||||
doc.flags.ignore_voucher_validation = True
|
||||
doc.submit()
|
||||
else:
|
||||
doc.save()
|
||||
|
||||
self.validate_qty(doc)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@@ -222,7 +222,6 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
sle.flags.ignore_permissions = 1
|
||||
sle.allow_negative_stock = allow_negative_stock
|
||||
sle.via_landed_cost_voucher = via_landed_cost_voucher
|
||||
sle.set_posting_datetime()
|
||||
sle.submit()
|
||||
|
||||
# Added to handle the case when the stock ledger entry is created from the repostig
|
||||
@@ -988,18 +987,23 @@ class update_entries_after:
|
||||
if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
|
||||
return
|
||||
|
||||
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||
|
||||
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
|
||||
doc.calculate_qty_and_amount(save=True)
|
||||
if self.args.get("sle_id") and sle.actual_qty < 0:
|
||||
doc = frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
sle.serial_and_batch_bundle,
|
||||
["total_amount", "total_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
else:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
|
||||
doc.calculate_qty_and_amount(save=True)
|
||||
|
||||
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
|
||||
|
||||
precision = doc.precision("total_qty")
|
||||
self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
|
||||
if flt(self.wh_data.qty_after_transaction, precision):
|
||||
self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
|
||||
self.wh_data.qty_after_transaction, precision
|
||||
self.wh_data.qty_after_transaction += flt(doc.total_qty, self.flt_precision)
|
||||
if flt(self.wh_data.qty_after_transaction, self.flt_precision):
|
||||
self.wh_data.valuation_rate = flt(self.wh_data.stock_value, self.flt_precision) / flt(
|
||||
self.wh_data.qty_after_transaction, self.flt_precision
|
||||
)
|
||||
|
||||
def update_valuation_rate_in_serial_and_batch_bundle(self, sle, valuation_rate):
|
||||
|
||||
@@ -5,10 +5,38 @@ frappe.provide("erpnext.buying");
|
||||
|
||||
erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Order");
|
||||
|
||||
// client script for Subcontracting Order Item is not necessarily required as the server side code will do everything that is necessary.
|
||||
// this is just so that the user does not get potentially confused
|
||||
frappe.ui.form.on("Subcontracting Order Item", {
|
||||
qty(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.rate);
|
||||
const service_item = frm.doc.service_items[row.idx - 1];
|
||||
frappe.model.set_value(
|
||||
service_item.doctype,
|
||||
service_item.name,
|
||||
"qty",
|
||||
row.qty * row.sc_conversion_factor
|
||||
);
|
||||
frappe.model.set_value(service_item.doctype, service_item.name, "fg_item_qty", row.qty);
|
||||
frappe.model.set_value(
|
||||
service_item.doctype,
|
||||
service_item.name,
|
||||
"amount",
|
||||
row.qty * row.sc_conversion_factor * service_item.rate
|
||||
);
|
||||
},
|
||||
before_items_remove(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
frm.toggle_enable(["service_items"], true);
|
||||
frm.get_field("service_items").grid.grid_rows[row.idx - 1].remove();
|
||||
frm.toggle_enable(["service_items"], false);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Subcontracting Order", {
|
||||
setup: (frm) => {
|
||||
frm.get_field("items").grid.cannot_add_rows = true;
|
||||
frm.get_field("items").grid.only_sortable();
|
||||
frm.trigger("set_queries");
|
||||
|
||||
frm.set_indicator_formatter("item_code", (doc) => (doc.qty <= doc.received_qty ? "green" : "orange"));
|
||||
|
||||
@@ -6,7 +6,6 @@ from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.stock_balance import update_bin_qty
|
||||
@@ -120,20 +119,15 @@ class SubcontractingOrder(SubcontractingController):
|
||||
def on_submit(self):
|
||||
self.update_prevdoc_status()
|
||||
self.update_status()
|
||||
self.update_sco_qty_in_po()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_prevdoc_status()
|
||||
self.update_status()
|
||||
self.update_sco_qty_in_po(cancel=True)
|
||||
|
||||
def validate_purchase_order_for_subcontracting(self):
|
||||
if self.purchase_order:
|
||||
if is_subcontracting_order_created(self.purchase_order):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one."
|
||||
)
|
||||
)
|
||||
|
||||
po = frappe.get_doc("Purchase Order", self.purchase_order)
|
||||
|
||||
if not po.is_subcontracted:
|
||||
@@ -154,10 +148,23 @@ class SubcontractingOrder(SubcontractingController):
|
||||
frappe.throw(_("Please select a Subcontracting Purchase Order."))
|
||||
|
||||
def validate_service_items(self):
|
||||
for item in self.service_items:
|
||||
if frappe.get_value("Item", item.item_code, "is_stock_item"):
|
||||
msg = f"Service Item {item.item_name} must be a non-stock item."
|
||||
frappe.throw(_(msg))
|
||||
purchase_order_items = [item.purchase_order_item for item in self.items]
|
||||
self.service_items = [
|
||||
service_item
|
||||
for service_item in self.service_items
|
||||
if service_item.purchase_order_item in purchase_order_items
|
||||
]
|
||||
|
||||
for service_item in self.service_items:
|
||||
if frappe.get_value("Item", service_item.item_code, "is_stock_item"):
|
||||
frappe.throw(_("Service Item {0} must be a non-stock item.").format(service_item.item_code))
|
||||
|
||||
item = next(
|
||||
item for item in self.items if item.purchase_order_item == service_item.purchase_order_item
|
||||
)
|
||||
service_item.qty = item.qty * item.sc_conversion_factor
|
||||
service_item.fg_item_qty = item.qty
|
||||
service_item.amount = service_item.qty * service_item.rate
|
||||
|
||||
def validate_supplied_items(self):
|
||||
if self.supplier_warehouse:
|
||||
@@ -241,6 +248,18 @@ class SubcontractingOrder(SubcontractingController):
|
||||
for si in self.service_items:
|
||||
if si.fg_item:
|
||||
item = frappe.get_doc("Item", si.fg_item)
|
||||
|
||||
po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item)
|
||||
available_qty = po_item.qty - po_item.sco_qty
|
||||
|
||||
if available_qty == 0:
|
||||
continue
|
||||
|
||||
si.qty = available_qty
|
||||
conversion_factor = po_item.qty / po_item.fg_item_qty
|
||||
si.fg_item_qty = available_qty / conversion_factor
|
||||
si.amount = available_qty * si.rate
|
||||
|
||||
bom = (
|
||||
frappe.db.get_value(
|
||||
"Subcontracting BOM",
|
||||
@@ -257,6 +276,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
"schedule_date": self.schedule_date,
|
||||
"description": item.description,
|
||||
"qty": si.fg_item_qty,
|
||||
"sc_conversion_factor": conversion_factor,
|
||||
"stock_uom": item.stock_uom,
|
||||
"bom": bom,
|
||||
"purchase_order_item": si.purchase_order_item,
|
||||
@@ -310,6 +330,12 @@ class SubcontractingOrder(SubcontractingController):
|
||||
self.update_ordered_qty_for_subcontracting()
|
||||
self.update_reserved_qty_for_subcontracting()
|
||||
|
||||
def update_sco_qty_in_po(self, cancel=False):
|
||||
for service_item in self.service_items:
|
||||
doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item)
|
||||
doc.sco_qty = (doc.sco_qty + service_item.qty) if not cancel else (doc.sco_qty - service_item.qty)
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_receipt(source_name, target_doc=None):
|
||||
|
||||
@@ -40,12 +40,6 @@ class TestSubcontractingOrder(FrappeTestCase):
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
def test_populate_items_table(self):
|
||||
sco = get_subcontracting_order()
|
||||
sco.items = None
|
||||
sco.populate_items_table()
|
||||
self.assertEqual(len(sco.service_items), len(sco.items))
|
||||
|
||||
def test_set_missing_values(self):
|
||||
sco = get_subcontracting_order()
|
||||
before = {sco.total_qty, sco.total, sco.total_additional_costs}
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"project",
|
||||
"section_break_34",
|
||||
"purchase_order_item",
|
||||
"page_break"
|
||||
"page_break",
|
||||
"sc_conversion_factor"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -144,8 +145,8 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"print_width": "60px",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "60px"
|
||||
},
|
||||
@@ -381,13 +382,20 @@
|
||||
"no_copy": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sc_conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "SC Conversion Factor",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-06 15:23:05.252346",
|
||||
"modified": "2024-12-13 13:35:28.935898",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Item",
|
||||
|
||||
@@ -42,6 +42,7 @@ class SubcontractingOrderItem(Document):
|
||||
received_qty: DF.Float
|
||||
returned_qty: DF.Float
|
||||
rm_cost_per_qty: DF.Currency
|
||||
sc_conversion_factor: DF.Float
|
||||
schedule_date: DF.Date | None
|
||||
service_cost_per_qty: DF.Currency
|
||||
stock_uom: DF.Link
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-30 13:29:31.017440",
|
||||
"modified": "2024-12-05 17:33:46.099601",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Service Item",
|
||||
|
||||
@@ -19,6 +19,8 @@ class SubcontractingOrderServiceItem(Document):
|
||||
fg_item_qty: DF.Float
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data
|
||||
material_request: DF.Link | None
|
||||
material_request_item: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
Reference in New Issue
Block a user