mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-19 04:59:18 +00:00
Merge pull request #38853 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -33,7 +33,9 @@
|
|||||||
},
|
},
|
||||||
"Stocks": {
|
"Stocks": {
|
||||||
"Mati\u00e8res premi\u00e8res": {},
|
"Mati\u00e8res premi\u00e8res": {},
|
||||||
"Stock de produits fini": {},
|
"Stock de produits fini": {
|
||||||
|
"account_type": "Stock"
|
||||||
|
},
|
||||||
"Stock exp\u00e9di\u00e9 non-factur\u00e9": {},
|
"Stock exp\u00e9di\u00e9 non-factur\u00e9": {},
|
||||||
"Travaux en cours": {},
|
"Travaux en cours": {},
|
||||||
"account_type": "Stock"
|
"account_type": "Stock"
|
||||||
@@ -395,9 +397,11 @@
|
|||||||
},
|
},
|
||||||
"Produits": {
|
"Produits": {
|
||||||
"Revenus de ventes": {
|
"Revenus de ventes": {
|
||||||
" Escomptes de volume sur ventes": {},
|
"Escomptes de volume sur ventes": {},
|
||||||
"Autres produits d'exploitation": {},
|
"Autres produits d'exploitation": {},
|
||||||
"Ventes": {},
|
"Ventes": {
|
||||||
|
"account_type": "Income Account"
|
||||||
|
},
|
||||||
"Ventes avec des provinces harmonis\u00e9es": {},
|
"Ventes avec des provinces harmonis\u00e9es": {},
|
||||||
"Ventes avec des provinces non-harmonis\u00e9es": {},
|
"Ventes avec des provinces non-harmonis\u00e9es": {},
|
||||||
"Ventes \u00e0 l'\u00e9tranger": {}
|
"Ventes \u00e0 l'\u00e9tranger": {}
|
||||||
|
|||||||
@@ -53,8 +53,13 @@
|
|||||||
},
|
},
|
||||||
"II. Forderungen und sonstige Vermögensgegenstände": {
|
"II. Forderungen und sonstige Vermögensgegenstände": {
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"Ford. a. Lieferungen und Leistungen": {
|
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||||
"account_number": "1400",
|
"account_number": "1400",
|
||||||
|
"account_type": "Receivable",
|
||||||
|
"is_group": 1
|
||||||
|
},
|
||||||
|
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||||
|
"account_number": "1410",
|
||||||
"account_type": "Receivable"
|
"account_type": "Receivable"
|
||||||
},
|
},
|
||||||
"Durchlaufende Posten": {
|
"Durchlaufende Posten": {
|
||||||
@@ -180,8 +185,13 @@
|
|||||||
},
|
},
|
||||||
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
|
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"Verbindlichkeiten aus Lieferungen u. Leistungen": {
|
"Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||||
"account_number": "1600",
|
"account_number": "1600",
|
||||||
|
"account_type": "Payable",
|
||||||
|
"is_group": 1
|
||||||
|
},
|
||||||
|
"Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||||
|
"account_number": "1610",
|
||||||
"account_type": "Payable"
|
"account_type": "Payable"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -407,13 +407,10 @@
|
|||||||
"Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": {
|
"Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": {
|
||||||
"account_number": "9960"
|
"account_number": "9960"
|
||||||
},
|
},
|
||||||
"Debitoren": {
|
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||||
"is_group": 1,
|
|
||||||
"account_number": "10000"
|
|
||||||
},
|
|
||||||
"Forderungen aus Lieferungen und Leistungen": {
|
|
||||||
"account_number": "1200",
|
"account_number": "1200",
|
||||||
"account_type": "Receivable"
|
"account_type": "Receivable",
|
||||||
|
"is_group": 1
|
||||||
},
|
},
|
||||||
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||||
"account_number": "1210"
|
"account_number": "1210"
|
||||||
@@ -1138,18 +1135,15 @@
|
|||||||
"Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": {
|
"Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": {
|
||||||
"account_number": "9964"
|
"account_number": "9964"
|
||||||
},
|
},
|
||||||
"Kreditoren": {
|
"Verb. aus Lieferungen und Leistungen mit Kontokorrent": {
|
||||||
"account_number": "70000",
|
"account_number": "3300",
|
||||||
|
"account_type": "Payable",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"Wareneingangs-Verrechnungskonto" : {
|
"Wareneingangs-Verrechnungskonto" : {
|
||||||
"account_number": "70001",
|
"account_number": "70001",
|
||||||
"account_type": "Stock Received But Not Billed"
|
"account_type": "Stock Received But Not Billed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Verb. aus Lieferungen und Leistungen": {
|
|
||||||
"account_number": "3300",
|
|
||||||
"account_type": "Payable"
|
|
||||||
},
|
|
||||||
"Verb. aus Lieferungen und Leistungen ohne Kontokorrent": {
|
"Verb. aus Lieferungen und Leistungen ohne Kontokorrent": {
|
||||||
"account_number": "3310"
|
"account_number": "3310"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1703,12 +1703,13 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list
|
|||||||
if not split_rows:
|
if not split_rows:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
frappe.msgprint(
|
if len(split_rows) > 1:
|
||||||
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
|
frappe.msgprint(
|
||||||
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
|
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
|
||||||
),
|
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
|
||||||
alert=True,
|
),
|
||||||
)
|
alert=True,
|
||||||
|
)
|
||||||
outstanding_invoices_after_split += split_rows
|
outstanding_invoices_after_split += split_rows
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -592,6 +592,27 @@ class PaymentReconciliation(Document):
|
|||||||
|
|
||||||
invoice_exchange_map.update(purchase_invoice_map)
|
invoice_exchange_map.update(purchase_invoice_map)
|
||||||
|
|
||||||
|
journals = [
|
||||||
|
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
|
||||||
|
]
|
||||||
|
journals.extend(
|
||||||
|
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
|
||||||
|
)
|
||||||
|
if journals:
|
||||||
|
journals = list(set(journals))
|
||||||
|
journals_map = frappe._dict(
|
||||||
|
frappe.db.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
|
||||||
|
fields=[
|
||||||
|
"parent as `name`",
|
||||||
|
"exchange_rate",
|
||||||
|
],
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
invoice_exchange_map.update(journals_map)
|
||||||
|
|
||||||
return invoice_exchange_map
|
return invoice_exchange_map
|
||||||
|
|
||||||
def validate_allocation(self):
|
def validate_allocation(self):
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"group_by",
|
"group_by",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"territory",
|
"territory",
|
||||||
|
"ignore_exchange_rate_revaluation_journals",
|
||||||
"column_break_14",
|
"column_break_14",
|
||||||
"to_date",
|
"to_date",
|
||||||
"finance_book",
|
"finance_book",
|
||||||
@@ -376,10 +377,16 @@
|
|||||||
"fieldname": "pdf_name",
|
"fieldname": "pdf_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "PDF Name"
|
"label": "PDF Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-28 12:59:53.071334",
|
"modified": "2023-12-18 12:20:08.965120",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts",
|
"name": "Process Statement Of Accounts",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class ProcessStatementOfAccounts(Document):
|
|||||||
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
|
||||||
from_date: DF.Date | None
|
from_date: DF.Date | None
|
||||||
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
|
||||||
|
ignore_exchange_rate_revaluation_journals: DF.Check
|
||||||
include_ageing: DF.Check
|
include_ageing: DF.Check
|
||||||
include_break: DF.Check
|
include_break: DF.Check
|
||||||
letter_head: DF.Link | None
|
letter_head: DF.Link | None
|
||||||
@@ -119,6 +120,18 @@ def get_statement_dict(doc, get_statement_dict=False):
|
|||||||
statement_dict = {}
|
statement_dict = {}
|
||||||
ageing = ""
|
ageing = ""
|
||||||
|
|
||||||
|
err_journals = None
|
||||||
|
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
|
||||||
|
err_journals = frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"company": doc.company,
|
||||||
|
"docstatus": 1,
|
||||||
|
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
|
||||||
|
},
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
|
||||||
for entry in doc.customers:
|
for entry in doc.customers:
|
||||||
if doc.include_ageing:
|
if doc.include_ageing:
|
||||||
ageing = set_ageing(doc, entry)
|
ageing = set_ageing(doc, entry)
|
||||||
@@ -131,6 +144,8 @@ def get_statement_dict(doc, get_statement_dict=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
filters = get_common_filters(doc)
|
filters = get_common_filters(doc)
|
||||||
|
if err_journals:
|
||||||
|
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
|
||||||
|
|
||||||
if doc.report == "General Ledger":
|
if doc.report == "General Ledger":
|
||||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||||
|
|||||||
@@ -2356,9 +2356,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_received_items(reference_name, doctype, reference_fieldname):
|
def get_received_items(reference_name, doctype, reference_fieldname):
|
||||||
|
reference_field = "inter_company_invoice_reference"
|
||||||
|
if doctype == "Purchase Order":
|
||||||
|
reference_field = "inter_company_order_reference"
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
reference_field: reference_name,
|
||||||
|
"docstatus": 1,
|
||||||
|
}
|
||||||
|
|
||||||
target_doctypes = frappe.get_all(
|
target_doctypes = frappe.get_all(
|
||||||
doctype,
|
doctype,
|
||||||
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
|
filters=filters,
|
||||||
as_list=True,
|
as_list=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -242,8 +242,12 @@ class ReceivablePayableReport(object):
|
|||||||
row.invoiced_in_account_currency += amount_in_account_currency
|
row.invoiced_in_account_currency += amount_in_account_currency
|
||||||
else:
|
else:
|
||||||
if self.is_invoice(ple):
|
if self.is_invoice(ple):
|
||||||
row.credit_note -= amount
|
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
|
||||||
row.credit_note_in_account_currency -= amount_in_account_currency
|
row.paid -= amount
|
||||||
|
row.paid_in_account_currency -= amount_in_account_currency
|
||||||
|
else:
|
||||||
|
row.credit_note -= amount
|
||||||
|
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||||
else:
|
else:
|
||||||
row.paid -= amount
|
row.paid -= amount
|
||||||
row.paid_in_account_currency -= amount_in_account_currency
|
row.paid_in_account_currency -= amount_in_account_currency
|
||||||
|
|||||||
@@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
|
|
||||||
return credit_note
|
return credit_note
|
||||||
|
|
||||||
|
def test_pos_receivable(self):
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": [self.customer],
|
||||||
|
"report_date": add_days(today(), 2),
|
||||||
|
"based_on_payment_terms": 0,
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
"show_remarks": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||||
|
pos_inv.posting_date = add_days(today(), 2)
|
||||||
|
pos_inv.is_pos = 1
|
||||||
|
pos_inv.append(
|
||||||
|
"payments",
|
||||||
|
frappe._dict(
|
||||||
|
mode_of_payment="Cash",
|
||||||
|
amount=flt(pos_inv.grand_total / 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
pos_inv.disable_rounded_total = 1
|
||||||
|
pos_inv.save()
|
||||||
|
pos_inv.submit()
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]
|
||||||
|
|
||||||
|
row = report[1][-1]
|
||||||
|
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
|
||||||
|
pos_inv.cancel()
|
||||||
|
|
||||||
def test_accounts_receivable(self):
|
def test_accounts_receivable(self):
|
||||||
filters = {
|
filters = {
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
|
|||||||
@@ -238,6 +238,9 @@ def get_conditions(filters):
|
|||||||
if filters.get("voucher_no"):
|
if filters.get("voucher_no"):
|
||||||
conditions.append("voucher_no=%(voucher_no)s")
|
conditions.append("voucher_no=%(voucher_no)s")
|
||||||
|
|
||||||
|
if filters.get("voucher_no_not_in"):
|
||||||
|
conditions.append("voucher_no not in %(voucher_no_not_in)s")
|
||||||
|
|
||||||
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
|
||||||
conditions.append("party_type in ('Customer', 'Supplier')")
|
conditions.append("party_type in ('Customer', 'Supplier')")
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
|||||||
"payable_account": inv.credit_to,
|
"payable_account": inv.credit_to,
|
||||||
"mode_of_payment": inv.mode_of_payment,
|
"mode_of_payment": inv.mode_of_payment,
|
||||||
"project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
|
"project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
|
||||||
|
"bill_no": inv.bill_no,
|
||||||
|
"bill_date": inv.bill_date,
|
||||||
"remarks": inv.remarks,
|
"remarks": inv.remarks,
|
||||||
"purchase_order": ", ".join(purchase_order),
|
"purchase_order": ", ".join(purchase_order),
|
||||||
"purchase_receipt": ", ".join(purchase_receipt),
|
"purchase_receipt": ", ".join(purchase_receipt),
|
||||||
|
|||||||
@@ -345,21 +345,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
|||||||
|
|
||||||
if filters.get("party"):
|
if filters.get("party"):
|
||||||
party = [filters.get("party")]
|
party = [filters.get("party")]
|
||||||
query = query.where(
|
jv_condition = gle.against.isin(party) | (
|
||||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
(gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))
|
||||||
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
|
|
||||||
| gle.party.isin(party)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||||
query = query.where(
|
jv_condition = gle.against.isin(party) | (
|
||||||
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
|
(gle.voucher_type == "Journal Entry")
|
||||||
| (
|
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
||||||
(gle.voucher_type == "Journal Entry")
|
|
||||||
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
|
|
||||||
)
|
|
||||||
| gle.party.isin(party)
|
|
||||||
)
|
)
|
||||||
|
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -657,8 +657,10 @@ def update_reference_in_payment_entry(
|
|||||||
"total_amount": d.grand_total,
|
"total_amount": d.grand_total,
|
||||||
"outstanding_amount": d.outstanding_amount,
|
"outstanding_amount": d.outstanding_amount,
|
||||||
"allocated_amount": d.allocated_amount,
|
"allocated_amount": d.allocated_amount,
|
||||||
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
"exchange_rate": d.exchange_rate
|
||||||
"exchange_gain_loss": d.exchange_gain_loss,
|
if d.difference_amount is not None
|
||||||
|
else payment_entry.get_exchange_rate(),
|
||||||
|
"exchange_gain_loss": d.difference_amount,
|
||||||
"account": d.account,
|
"account": d.account,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ frappe.ui.form.on("Purchase Order Item", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fg_item_qty: async function(frm, cdt, cdn) {
|
qty: async function (frm, cdt, cdn) {
|
||||||
if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
|
if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
|
||||||
var row = locals[cdt][cdn];
|
var row = locals[cdt][cdn];
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ frappe.ui.form.on("Purchase Order Item", {
|
|||||||
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item)
|
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item)
|
||||||
|
|
||||||
if (result.message && row.item_code == result.message.service_item && row.uom == result.message.service_item_uom) {
|
if (result.message && row.item_code == result.message.service_item && row.uom == result.message.service_item_uom) {
|
||||||
frappe.model.set_value(cdt, cdn, "qty", flt(row.fg_item_qty) * flt(result.message.conversion_factor));
|
frappe.model.set_value(cdt, cdn, "fg_item_qty", flt(row.qty) / flt(result.message.conversion_factor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,15 @@ class RequestforQuotation(BuyingController):
|
|||||||
supplier.quote_status = "Pending"
|
supplier.quote_status = "Pending"
|
||||||
self.send_to_supplier()
|
self.send_to_supplier()
|
||||||
|
|
||||||
|
def before_print(self, settings=None):
|
||||||
|
"""Use the first suppliers data to render the print preview."""
|
||||||
|
if self.vendor or not self.suppliers:
|
||||||
|
# If a specific supplier is already set, via Tools > Download PDF,
|
||||||
|
# we don't want to override it.
|
||||||
|
return
|
||||||
|
|
||||||
|
self.update_supplier_part_no(self.suppliers[0].supplier)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.db_set("status", "Cancelled")
|
self.db_set("status", "Cancelled")
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ class AccountsController(TransactionBase):
|
|||||||
self.disable_pricing_rule_on_internal_transfer()
|
self.disable_pricing_rule_on_internal_transfer()
|
||||||
self.disable_tax_included_prices_for_internal_transfer()
|
self.disable_tax_included_prices_for_internal_transfer()
|
||||||
self.set_incoming_rate()
|
self.set_incoming_rate()
|
||||||
|
self.init_internal_values()
|
||||||
|
|
||||||
if self.meta.get_field("currency"):
|
if self.meta.get_field("currency"):
|
||||||
self.calculate_taxes_and_totals()
|
self.calculate_taxes_and_totals()
|
||||||
@@ -225,6 +226,16 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
self.set_total_in_words()
|
self.set_total_in_words()
|
||||||
|
|
||||||
|
def init_internal_values(self):
|
||||||
|
# init all the internal values as 0 on sa
|
||||||
|
if self.docstatus.is_draft():
|
||||||
|
# TODO: Add all such pending values here
|
||||||
|
fields = ["billed_amt", "delivered_qty"]
|
||||||
|
for item in self.get("items"):
|
||||||
|
for field in fields:
|
||||||
|
if hasattr(item, field):
|
||||||
|
item.set(field, 0)
|
||||||
|
|
||||||
def before_cancel(self):
|
def before_cancel(self):
|
||||||
validate_einvoice_fields(self)
|
validate_einvoice_fields(self)
|
||||||
|
|
||||||
@@ -639,6 +650,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
args["doctype"] = self.doctype
|
args["doctype"] = self.doctype
|
||||||
args["name"] = self.name
|
args["name"] = self.name
|
||||||
|
args["child_doctype"] = item.doctype
|
||||||
args["child_docname"] = item.name
|
args["child_docname"] = item.name
|
||||||
args["ignore_pricing_rule"] = (
|
args["ignore_pricing_rule"] = (
|
||||||
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
|
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
|
||||||
|
|||||||
@@ -381,7 +381,11 @@ class BuyingController(SubcontractingController):
|
|||||||
|
|
||||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||||
else:
|
else:
|
||||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
field = (
|
||||||
|
"incoming_rate"
|
||||||
|
if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
|
||||||
|
else "rate"
|
||||||
|
)
|
||||||
rate = flt(
|
rate = flt(
|
||||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||||
* (d.conversion_factor or 1),
|
* (d.conversion_factor or 1),
|
||||||
|
|||||||
@@ -56,10 +56,24 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part
|
|||||||
|
|
||||||
copy_attributes_to_variant(template, variant)
|
copy_attributes_to_variant(template, variant)
|
||||||
|
|
||||||
variant.manufacturer = manufacturer
|
|
||||||
variant.manufacturer_part_no = manufacturer_part_no
|
|
||||||
|
|
||||||
variant.item_code = append_number_if_name_exists("Item", template.name)
|
variant.item_code = append_number_if_name_exists("Item", template.name)
|
||||||
|
variant.flags.ignore_mandatory = True
|
||||||
|
variant.save()
|
||||||
|
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer}
|
||||||
|
):
|
||||||
|
manufacturer_doc = frappe.new_doc("Item Manufacturer")
|
||||||
|
manufacturer_doc.update(
|
||||||
|
{
|
||||||
|
"item_code": variant.name,
|
||||||
|
"manufacturer": manufacturer,
|
||||||
|
"manufacturer_part_no": manufacturer_part_no,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
manufacturer_doc.flags.ignore_mandatory = True
|
||||||
|
manufacturer_doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
return variant
|
return variant
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from frappe.model.meta import get_field_precision
|
|||||||
from frappe.utils import flt, format_datetime, get_datetime
|
from frappe.utils import flt, format_datetime, get_datetime
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||||
|
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
from erpnext.stock.utils import get_incoming_rate
|
||||||
|
|
||||||
|
|
||||||
@@ -69,8 +71,6 @@ def validate_return_against(doc):
|
|||||||
|
|
||||||
|
|
||||||
def validate_returned_items(doc):
|
def validate_returned_items(doc):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
|
||||||
|
|
||||||
valid_items = frappe._dict()
|
valid_items = frappe._dict()
|
||||||
|
|
||||||
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor"
|
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor"
|
||||||
@@ -123,26 +123,6 @@ def validate_returned_items(doc):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif ref.batch_no and d.batch_no not in ref.batch_no:
|
|
||||||
frappe.throw(
|
|
||||||
_("Row # {0}: Batch No must be same as {1} {2}").format(
|
|
||||||
d.idx, doc.doctype, doc.return_against
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif ref.serial_no:
|
|
||||||
if d.qty and not d.serial_no:
|
|
||||||
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
|
|
||||||
else:
|
|
||||||
serial_nos = get_serial_nos(d.serial_no)
|
|
||||||
for s in serial_nos:
|
|
||||||
if s not in ref.serial_no:
|
|
||||||
frappe.throw(
|
|
||||||
_("Row # {0}: Serial No {1} does not match with {2} {3}").format(
|
|
||||||
d.idx, s, doc.doctype, doc.return_against
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
warehouse_mandatory
|
warehouse_mandatory
|
||||||
and not d.get("warehouse")
|
and not d.get("warehouse")
|
||||||
@@ -397,71 +377,92 @@ def make_return_doc(
|
|||||||
else:
|
else:
|
||||||
doc.run_method("calculate_taxes_and_totals")
|
doc.run_method("calculate_taxes_and_totals")
|
||||||
|
|
||||||
def update_item(source_doc, target_doc, source_parent):
|
def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field):
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||||
|
|
||||||
target_doc.qty = -1 * source_doc.qty
|
|
||||||
item_details = frappe.get_cached_value(
|
|
||||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
|
||||||
)
|
|
||||||
|
|
||||||
returned_serial_nos = []
|
returned_serial_nos = []
|
||||||
if source_doc.get("serial_and_batch_bundle"):
|
returned_batches = frappe._dict()
|
||||||
if item_details.has_serial_no:
|
serial_and_batch_field = (
|
||||||
returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
|
"serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle"
|
||||||
|
)
|
||||||
|
old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no"
|
||||||
|
old_batch_no_field = "batch_no"
|
||||||
|
|
||||||
type_of_transaction = "Inward"
|
if (
|
||||||
if (
|
source_doc.get(serial_and_batch_field)
|
||||||
frappe.db.get_value(
|
or source_doc.get(old_serial_no_field)
|
||||||
"Serial and Batch Bundle", source_doc.serial_and_batch_bundle, "type_of_transaction"
|
or source_doc.get(old_batch_no_field)
|
||||||
)
|
):
|
||||||
== "Inward"
|
|
||||||
):
|
|
||||||
type_of_transaction = "Outward"
|
|
||||||
|
|
||||||
cls_obj = SerialBatchCreation(
|
|
||||||
{
|
|
||||||
"type_of_transaction": type_of_transaction,
|
|
||||||
"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
|
|
||||||
"returned_against": source_doc.name,
|
|
||||||
"item_code": source_doc.item_code,
|
|
||||||
"returned_serial_nos": returned_serial_nos,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
cls_obj.duplicate_package()
|
|
||||||
if cls_obj.serial_and_batch_bundle:
|
|
||||||
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
|
|
||||||
|
|
||||||
if source_doc.get("rejected_serial_and_batch_bundle"):
|
|
||||||
if item_details.has_serial_no:
|
if item_details.has_serial_no:
|
||||||
returned_serial_nos = get_returned_serial_nos(
|
returned_serial_nos = get_returned_serial_nos(
|
||||||
source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle"
|
source_doc, source_parent, serial_no_field=serial_and_batch_field
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
returned_batches = get_returned_batches(
|
||||||
|
source_doc, source_parent, batch_no_field=serial_and_batch_field
|
||||||
)
|
)
|
||||||
|
|
||||||
type_of_transaction = "Inward"
|
type_of_transaction = "Inward"
|
||||||
if (
|
if source_doc.get(serial_and_batch_field) and (
|
||||||
frappe.db.get_value(
|
frappe.db.get_value(
|
||||||
"Serial and Batch Bundle", source_doc.rejected_serial_and_batch_bundle, "type_of_transaction"
|
"Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction"
|
||||||
)
|
)
|
||||||
== "Inward"
|
== "Inward"
|
||||||
):
|
):
|
||||||
type_of_transaction = "Outward"
|
type_of_transaction = "Outward"
|
||||||
|
elif source_parent.doctype in [
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Purchase Receipt",
|
||||||
|
"Subcontracting Receipt",
|
||||||
|
]:
|
||||||
|
type_of_transaction = "Outward"
|
||||||
|
|
||||||
cls_obj = SerialBatchCreation(
|
cls_obj = SerialBatchCreation(
|
||||||
{
|
{
|
||||||
"type_of_transaction": type_of_transaction,
|
"type_of_transaction": type_of_transaction,
|
||||||
"serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
|
"serial_and_batch_bundle": source_doc.get(serial_and_batch_field),
|
||||||
"returned_against": source_doc.name,
|
"returned_against": source_doc.name,
|
||||||
"item_code": source_doc.item_code,
|
"item_code": source_doc.item_code,
|
||||||
"returned_serial_nos": returned_serial_nos,
|
"returned_serial_nos": returned_serial_nos,
|
||||||
|
"voucher_type": source_parent.doctype,
|
||||||
|
"do_not_submit": True,
|
||||||
|
"warehouse": source_doc.warehouse,
|
||||||
|
"has_serial_no": item_details.has_serial_no,
|
||||||
|
"has_batch_no": item_details.has_batch_no,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
cls_obj.duplicate_package()
|
serial_nos = []
|
||||||
if cls_obj.serial_and_batch_bundle:
|
batches = frappe._dict()
|
||||||
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
|
if source_doc.get(old_batch_no_field):
|
||||||
|
batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)})
|
||||||
|
elif source_doc.get(old_serial_no_field):
|
||||||
|
serial_nos = get_serial_nos(source_doc.get(old_serial_no_field))
|
||||||
|
elif source_doc.get(serial_and_batch_field):
|
||||||
|
if item_details.has_serial_no:
|
||||||
|
serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field))
|
||||||
|
else:
|
||||||
|
batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field))
|
||||||
|
|
||||||
|
if serial_nos:
|
||||||
|
cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos)))
|
||||||
|
elif batches:
|
||||||
|
for batch in batches:
|
||||||
|
if batch in returned_batches:
|
||||||
|
batches[batch] -= flt(returned_batches.get(batch))
|
||||||
|
|
||||||
|
cls_obj.batches = batches
|
||||||
|
|
||||||
|
if source_doc.get(serial_and_batch_field):
|
||||||
|
cls_obj.duplicate_package()
|
||||||
|
if cls_obj.serial_and_batch_bundle:
|
||||||
|
target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle)
|
||||||
|
else:
|
||||||
|
target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name)
|
||||||
|
|
||||||
|
def update_item(source_doc, target_doc, source_parent):
|
||||||
|
target_doc.qty = -1 * source_doc.qty
|
||||||
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
|
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
|
||||||
returned_qty_map = get_returned_qty_map_for_row(
|
returned_qty_map = get_returned_qty_map_for_row(
|
||||||
source_parent.name, source_parent.supplier, source_doc.name, doctype
|
source_parent.name, source_parent.supplier, source_doc.name, doctype
|
||||||
@@ -561,6 +562,17 @@ def make_return_doc(
|
|||||||
if default_warehouse_for_sales_return:
|
if default_warehouse_for_sales_return:
|
||||||
target_doc.warehouse = default_warehouse_for_sales_return
|
target_doc.warehouse = default_warehouse_for_sales_return
|
||||||
|
|
||||||
|
item_details = frappe.get_cached_value(
|
||||||
|
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||||
|
return
|
||||||
|
|
||||||
|
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||||
|
if target_doc.get(qty_field):
|
||||||
|
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
||||||
|
|
||||||
def update_terms(source_doc, target_doc, source_parent):
|
def update_terms(source_doc, target_doc, source_parent):
|
||||||
target_doc.payment_amount = -source_doc.payment_amount
|
target_doc.payment_amount = -source_doc.payment_amount
|
||||||
|
|
||||||
@@ -716,6 +728,9 @@ def get_returned_serial_nos(
|
|||||||
[parent_doc.doctype, "docstatus", "=", 1],
|
[parent_doc.doctype, "docstatus", "=", 1],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if serial_no_field == "rejected_serial_and_batch_bundle":
|
||||||
|
filters.append([child_doc.doctype, "rejected_qty", ">", 0])
|
||||||
|
|
||||||
# Required for POS Invoice
|
# Required for POS Invoice
|
||||||
if ignore_voucher_detail_no:
|
if ignore_voucher_detail_no:
|
||||||
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
|
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
|
||||||
@@ -723,9 +738,57 @@ def get_returned_serial_nos(
|
|||||||
ids = []
|
ids = []
|
||||||
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
|
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
|
||||||
ids.append(row.get("serial_and_batch_bundle"))
|
ids.append(row.get("serial_and_batch_bundle"))
|
||||||
if row.get(old_field):
|
if row.get(old_field) and not row.get(serial_no_field):
|
||||||
serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field)))
|
serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field)))
|
||||||
|
|
||||||
serial_nos.extend(get_serial_nos(ids))
|
if ids:
|
||||||
|
serial_nos.extend(get_serial_nos(ids))
|
||||||
|
|
||||||
return serial_nos
|
return serial_nos
|
||||||
|
|
||||||
|
|
||||||
|
def get_returned_batches(
|
||||||
|
child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None
|
||||||
|
):
|
||||||
|
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||||
|
|
||||||
|
batches = frappe._dict()
|
||||||
|
|
||||||
|
old_field = "batch_no"
|
||||||
|
if not batch_no_field:
|
||||||
|
batch_no_field = "serial_and_batch_bundle"
|
||||||
|
|
||||||
|
return_ref_field = frappe.scrub(child_doc.doctype)
|
||||||
|
if child_doc.doctype == "Delivery Note Item":
|
||||||
|
return_ref_field = "dn_detail"
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
f"`{'tab' + child_doc.doctype}`.`{batch_no_field}`",
|
||||||
|
f"`{'tab' + child_doc.doctype}`.`batch_no`",
|
||||||
|
f"`{'tab' + child_doc.doctype}`.`stock_qty`",
|
||||||
|
]
|
||||||
|
|
||||||
|
filters = [
|
||||||
|
[parent_doc.doctype, "return_against", "=", parent_doc.name],
|
||||||
|
[parent_doc.doctype, "is_return", "=", 1],
|
||||||
|
[child_doc.doctype, return_ref_field, "=", child_doc.name],
|
||||||
|
[parent_doc.doctype, "docstatus", "=", 1],
|
||||||
|
]
|
||||||
|
|
||||||
|
if batch_no_field == "rejected_serial_and_batch_bundle":
|
||||||
|
filters.append([child_doc.doctype, "rejected_qty", ">", 0])
|
||||||
|
|
||||||
|
# Required for POS Invoice
|
||||||
|
if ignore_voucher_detail_no:
|
||||||
|
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
|
||||||
|
|
||||||
|
ids = []
|
||||||
|
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
|
||||||
|
ids.append(row.get("serial_and_batch_bundle"))
|
||||||
|
if row.get(old_field) and not row.get(batch_no_field):
|
||||||
|
batches.setdefault(row.get(old_field), row.get("stock_qty"))
|
||||||
|
|
||||||
|
if ids:
|
||||||
|
batches.update(get_batches_from_bundle(ids))
|
||||||
|
|
||||||
|
return batches
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
|||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
from erpnext.stock.doctype.item.item import set_item_default
|
from erpnext.stock.doctype.item.item import set_item_default
|
||||||
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||||
|
|
||||||
|
|
||||||
class SellingController(StockController):
|
class SellingController(StockController):
|
||||||
@@ -308,6 +308,8 @@ class SellingController(StockController):
|
|||||||
"warehouse": p.warehouse or d.warehouse,
|
"warehouse": p.warehouse or d.warehouse,
|
||||||
"item_code": p.item_code,
|
"item_code": p.item_code,
|
||||||
"qty": flt(p.qty),
|
"qty": flt(p.qty),
|
||||||
|
"serial_no": p.serial_no if self.docstatus == 2 else None,
|
||||||
|
"batch_no": p.batch_no if self.docstatus == 2 else None,
|
||||||
"uom": p.uom,
|
"uom": p.uom,
|
||||||
"serial_and_batch_bundle": p.serial_and_batch_bundle
|
"serial_and_batch_bundle": p.serial_and_batch_bundle
|
||||||
or get_serial_and_batch_bundle(p, self),
|
or get_serial_and_batch_bundle(p, self),
|
||||||
@@ -330,6 +332,8 @@ class SellingController(StockController):
|
|||||||
"warehouse": d.warehouse,
|
"warehouse": d.warehouse,
|
||||||
"item_code": d.item_code,
|
"item_code": d.item_code,
|
||||||
"qty": d.stock_qty,
|
"qty": d.stock_qty,
|
||||||
|
"serial_no": d.serial_no if self.docstatus == 2 else None,
|
||||||
|
"batch_no": d.batch_no if self.docstatus == 2 else None,
|
||||||
"uom": d.uom,
|
"uom": d.uom,
|
||||||
"stock_uom": d.stock_uom,
|
"stock_uom": d.stock_uom,
|
||||||
"conversion_factor": d.conversion_factor,
|
"conversion_factor": d.conversion_factor,
|
||||||
@@ -428,11 +432,13 @@ class SellingController(StockController):
|
|||||||
|
|
||||||
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 self.get("return_against"):
|
if not self.get("return_against") or (
|
||||||
|
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||||
|
):
|
||||||
# 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"))
|
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
|
||||||
|
|
||||||
if not (self.get("is_return") and d.incoming_rate):
|
if not d.incoming_rate:
|
||||||
d.incoming_rate = get_incoming_rate(
|
d.incoming_rate = get_incoming_rate(
|
||||||
{
|
{
|
||||||
"item_code": d.item_code,
|
"item_code": d.item_code,
|
||||||
|
|||||||
@@ -455,6 +455,12 @@ class StockController(AccountsController):
|
|||||||
sl_dict.update(args)
|
sl_dict.update(args)
|
||||||
self.update_inventory_dimensions(d, sl_dict)
|
self.update_inventory_dimensions(d, sl_dict)
|
||||||
|
|
||||||
|
if self.docstatus == 2:
|
||||||
|
# To handle denormalized serial no records, will br deprecated in v16
|
||||||
|
for field in ["serial_no", "batch_no"]:
|
||||||
|
if d.get(field):
|
||||||
|
sl_dict[field] = d.get(field)
|
||||||
|
|
||||||
return sl_dict
|
return sl_dict
|
||||||
|
|
||||||
def update_inventory_dimensions(self, row, sl_dict) -> None:
|
def update_inventory_dimensions(self, row, sl_dict) -> None:
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
"idx": 5,
|
"idx": 5,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-28 22:28:00.104413",
|
"modified": "2023-12-01 18:46:49.468526",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Lead",
|
"name": "Lead",
|
||||||
@@ -577,6 +577,7 @@
|
|||||||
],
|
],
|
||||||
"search_fields": "lead_name,lead_owner,status",
|
"search_fields": "lead_name,lead_owner,status",
|
||||||
"sender_field": "email_id",
|
"sender_field": "email_id",
|
||||||
|
"sender_name_field": "lead_name",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_ema
|
|||||||
from erpnext.accounts.party import set_taxes
|
from erpnext.accounts.party import set_taxes
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||||
|
from erpnext.selling.doctype.customer.customer import parse_full_name
|
||||||
|
|
||||||
|
|
||||||
class Lead(SellingController, CRMNote):
|
class Lead(SellingController, CRMNote):
|
||||||
@@ -111,6 +112,10 @@ class Lead(SellingController, CRMNote):
|
|||||||
return
|
return
|
||||||
self.contact_doc = self.create_contact()
|
self.contact_doc = self.create_contact()
|
||||||
|
|
||||||
|
# leads created by email inbox only have the full name set
|
||||||
|
if self.lead_name and not any([self.first_name, self.middle_name, self.last_name]):
|
||||||
|
self.first_name, self.middle_name, self.last_name = parse_full_name(self.lead_name)
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
self.link_to_contact()
|
self.link_to_contact()
|
||||||
|
|
||||||
|
|||||||
@@ -583,6 +583,7 @@ class ProductionPlan(Document):
|
|||||||
|
|
||||||
if close:
|
if close:
|
||||||
self.db_set("status", "Closed")
|
self.db_set("status", "Closed")
|
||||||
|
self.update_bin_qty()
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.total_produced_qty > 0:
|
if self.total_produced_qty > 0:
|
||||||
@@ -597,6 +598,9 @@ class ProductionPlan(Document):
|
|||||||
if close is not None:
|
if close is not None:
|
||||||
self.db_set("status", self.status)
|
self.db_set("status", self.status)
|
||||||
|
|
||||||
|
if self.docstatus == 1 and self.status != "Completed":
|
||||||
|
self.update_bin_qty()
|
||||||
|
|
||||||
def update_ordered_status(self):
|
def update_ordered_status(self):
|
||||||
update_status = False
|
update_status = False
|
||||||
for d in self.po_items:
|
for d in self.po_items:
|
||||||
|
|||||||
@@ -1458,6 +1458,47 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
self.assertEqual(row.get("uom"), "Nos")
|
self.assertEqual(row.get("uom"), "Nos")
|
||||||
self.assertEqual(row.get("conversion_factor"), 10.0)
|
self.assertEqual(row.get("conversion_factor"), 10.0)
|
||||||
|
|
||||||
|
def test_unreserve_qty_on_closing_of_pp(self):
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
from erpnext.stock.utils import get_or_make_bin
|
||||||
|
|
||||||
|
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||||
|
rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
|
||||||
|
|
||||||
|
store_warehouse = create_warehouse("Store Warehouse", company="_Test Company")
|
||||||
|
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
|
||||||
|
|
||||||
|
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
|
||||||
|
|
||||||
|
pln = create_production_plan(
|
||||||
|
item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1
|
||||||
|
)
|
||||||
|
|
||||||
|
pln.for_warehouse = rm_warehouse
|
||||||
|
mr_items = get_items_for_material_requests(pln.as_dict())
|
||||||
|
for d in mr_items:
|
||||||
|
pln.append("mr_items", d)
|
||||||
|
|
||||||
|
pln.save()
|
||||||
|
pln.submit()
|
||||||
|
|
||||||
|
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||||
|
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||||
|
|
||||||
|
pln.reload()
|
||||||
|
pln.set_status(close=True)
|
||||||
|
|
||||||
|
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||||
|
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||||
|
self.assertAlmostEqual(after_qty, before_qty - 10)
|
||||||
|
|
||||||
|
pln.reload()
|
||||||
|
pln.set_status(close=False)
|
||||||
|
|
||||||
|
bin_name = get_or_make_bin(rm_item, rm_warehouse)
|
||||||
|
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||||
|
self.assertAlmostEqual(after_qty, before_qty)
|
||||||
|
|
||||||
|
|
||||||
def create_production_plan(**args):
|
def create_production_plan(**args):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -352,3 +352,4 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field
|
|||||||
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
|
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
|
||||||
# below migration patch should always run last
|
# below migration patch should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
|
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import frappe
|
|||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
|
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
|
||||||
|
frappe.reload_doc("assets", "doctype", "Asset Finance Book")
|
||||||
|
|
||||||
assets = get_details_of_draft_or_submitted_depreciable_assets()
|
assets = get_details_of_draft_or_submitted_depreciable_assets()
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ def get_asset_finance_books_map():
|
|||||||
afb.frequency_of_depreciation,
|
afb.frequency_of_depreciation,
|
||||||
afb.rate_of_depreciation,
|
afb.rate_of_depreciation,
|
||||||
afb.expected_value_after_useful_life,
|
afb.expected_value_after_useful_life,
|
||||||
|
afb.daily_prorata_based,
|
||||||
afb.shift_based,
|
afb.shift_based,
|
||||||
)
|
)
|
||||||
.where(asset.docstatus < 2)
|
.where(asset.docstatus < 2)
|
||||||
|
|||||||
@@ -2,14 +2,6 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Homepage', {
|
frappe.ui.form.on('Homepage', {
|
||||||
setup: function(frm) {
|
|
||||||
frm.fields_dict["products"].grid.get_field("item").get_query = function() {
|
|
||||||
return {
|
|
||||||
filters: {'published': 1}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frm.add_custom_button(__('Set Meta Tags'), () => {
|
frm.add_custom_button(__('Set Meta Tags'), () => {
|
||||||
frappe.utils.set_meta_tag('home');
|
frappe.utils.set_meta_tag('home');
|
||||||
|
|||||||
@@ -361,9 +361,14 @@ erpnext.buying = {
|
|||||||
new erpnext.SerialBatchPackageSelector(
|
new erpnext.SerialBatchPackageSelector(
|
||||||
me.frm, item, (r) => {
|
me.frm, item, (r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
|
let qty = Math.abs(r.total_qty);
|
||||||
|
if (doc.is_return) {
|
||||||
|
qty = qty * -1;
|
||||||
|
}
|
||||||
|
|
||||||
let update_values = {
|
let update_values = {
|
||||||
"serial_and_batch_bundle": r.name,
|
"serial_and_batch_bundle": r.name,
|
||||||
"qty": Math.abs(r.total_qty)
|
"qty": qty
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.warehouse) {
|
if (r.warehouse) {
|
||||||
@@ -396,9 +401,14 @@ erpnext.buying = {
|
|||||||
new erpnext.SerialBatchPackageSelector(
|
new erpnext.SerialBatchPackageSelector(
|
||||||
me.frm, item, (r) => {
|
me.frm, item, (r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
|
let qty = Math.abs(r.total_qty);
|
||||||
|
if (doc.is_return) {
|
||||||
|
qty = qty * -1;
|
||||||
|
}
|
||||||
|
|
||||||
let update_values = {
|
let update_values = {
|
||||||
"serial_and_batch_bundle": r.name,
|
"serial_and_batch_bundle": r.name,
|
||||||
"rejected_qty": Math.abs(r.total_qty)
|
"rejected_qty": qty
|
||||||
}
|
}
|
||||||
|
|
||||||
if (r.warehouse) {
|
if (r.warehouse) {
|
||||||
|
|||||||
@@ -380,6 +380,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
scan_barcode() {
|
scan_barcode() {
|
||||||
|
frappe.flags.dialog_set = false;
|
||||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
||||||
barcode_scanner.process_scan();
|
barcode_scanner.process_scan();
|
||||||
}
|
}
|
||||||
@@ -512,6 +513,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
cost_center: item.cost_center,
|
cost_center: item.cost_center,
|
||||||
tax_category: me.frm.doc.tax_category,
|
tax_category: me.frm.doc.tax_category,
|
||||||
item_tax_template: item.item_tax_template,
|
item_tax_template: item.item_tax_template,
|
||||||
|
child_doctype: item.doctype,
|
||||||
child_docname: item.name,
|
child_docname: item.name,
|
||||||
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
|
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1077,7 +1077,7 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function get_time_left(timestamp, agreement_status) {
|
function get_time_left(timestamp, agreement_status) {
|
||||||
const diff = moment(timestamp).diff(moment());
|
const diff = moment(timestamp).diff(frappe.datetime.system_datetime(true));
|
||||||
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed';
|
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed';
|
||||||
let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green';
|
let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green';
|
||||||
return {'diff_display': diff_display, 'indicator': indicator};
|
return {'diff_display': diff_display, 'indicator': indicator};
|
||||||
|
|||||||
@@ -114,13 +114,13 @@ 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_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, row.item_code, qty);
|
this.show_scan_message(row.idx, row.item_code, qty);
|
||||||
}),
|
}),
|
||||||
() => this.set_barcode_uom(row, uom),
|
() => this.set_barcode_uom(row, uom),
|
||||||
() => this.set_serial_no(row, serial_no),
|
|
||||||
() => this.set_batch_no(row, batch_no),
|
|
||||||
() => this.set_barcode(row, barcode),
|
|
||||||
() => this.clean_up(),
|
() => this.clean_up(),
|
||||||
() => this.revert_selector_flag(),
|
() => this.revert_selector_flag(),
|
||||||
() => resolve(row)
|
() => resolve(row)
|
||||||
@@ -131,10 +131,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
|||||||
// batch and serial selector is reduandant when all info can be added by scan
|
// batch and serial selector is reduandant when all info can be added by scan
|
||||||
// this flag on item row is used by transaction.js to avoid triggering selector
|
// this flag on item row is used by transaction.js to avoid triggering selector
|
||||||
set_selector_trigger_flag(data) {
|
set_selector_trigger_flag(data) {
|
||||||
const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
|
const {has_batch_no, has_serial_no} = data;
|
||||||
|
|
||||||
const require_selecting_batch = has_batch_no && !batch_no;
|
const require_selecting_batch = has_batch_no;
|
||||||
const require_selecting_serial = has_serial_no && !serial_no;
|
const require_selecting_serial = has_serial_no;
|
||||||
|
|
||||||
if (!(require_selecting_batch || require_selecting_serial)) {
|
if (!(require_selecting_batch || require_selecting_serial)) {
|
||||||
frappe.flags.hide_serial_batch_dialog = true;
|
frappe.flags.hide_serial_batch_dialog = true;
|
||||||
|
|||||||
@@ -317,9 +317,14 @@ erpnext.sales_common = {
|
|||||||
new erpnext.SerialBatchPackageSelector(
|
new erpnext.SerialBatchPackageSelector(
|
||||||
me.frm, item, (r) => {
|
me.frm, item, (r) => {
|
||||||
if (r) {
|
if (r) {
|
||||||
|
let qty = Math.abs(r.total_qty);
|
||||||
|
if (doc.is_return) {
|
||||||
|
qty = qty * -1;
|
||||||
|
}
|
||||||
|
|
||||||
frappe.model.set_value(item.doctype, item.name, {
|
frappe.model.set_value(item.doctype, item.name, {
|
||||||
"serial_and_batch_bundle": r.name,
|
"serial_and_batch_bundle": r.name,
|
||||||
"qty": Math.abs(r.total_qty)
|
"qty": qty
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,20 +31,40 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
secondary_action: () => this.edit_full_form(),
|
secondary_action: () => this.edit_full_form(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty;
|
|
||||||
this.dialog.set_value("qty", qty).then(() => {
|
|
||||||
if (this.item.serial_no) {
|
|
||||||
this.dialog.set_value("scan_serial_no", this.item.serial_no);
|
|
||||||
frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
|
|
||||||
} else if (this.item.batch_no) {
|
|
||||||
this.dialog.set_value("scan_batch_no", this.item.batch_no);
|
|
||||||
frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dialog.show();
|
this.dialog.show();
|
||||||
this.$scan_btn = this.dialog.$wrapper.find(".link-btn");
|
this.$scan_btn = this.dialog.$wrapper.find(".link-btn");
|
||||||
this.$scan_btn.css("display", "inline");
|
this.$scan_btn.css("display", "inline");
|
||||||
|
|
||||||
|
let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty;
|
||||||
|
|
||||||
|
if (this.item?.is_rejected) {
|
||||||
|
qty = this.item.rejected_qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
qty = Math.abs(qty);
|
||||||
|
if (qty > 0) {
|
||||||
|
this.dialog.set_value("qty", qty).then(() => {
|
||||||
|
if (this.item.serial_no && !this.item.serial_and_batch_bundle) {
|
||||||
|
let serial_nos = this.item.serial_no.split('\n');
|
||||||
|
if (serial_nos.length > 1) {
|
||||||
|
serial_nos.forEach(serial_no => {
|
||||||
|
this.dialog.fields_dict.entries.df.data.push({
|
||||||
|
serial_no: serial_no,
|
||||||
|
batch_no: this.item.batch_no
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.dialog.set_value("scan_serial_no", this.item.serial_no);
|
||||||
|
}
|
||||||
|
frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
|
||||||
|
} else if (this.item.batch_no && !this.item.serial_and_batch_bundle) {
|
||||||
|
this.dialog.set_value("scan_batch_no", this.item.batch_no);
|
||||||
|
frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialog.fields_dict.entries.grid.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get_serial_no_filters() {
|
get_serial_no_filters() {
|
||||||
@@ -464,13 +484,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_data() {
|
render_data() {
|
||||||
if (!this.frm.is_new() && this.bundle) {
|
if (this.bundle) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers',
|
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers',
|
||||||
args: {
|
args: {
|
||||||
item_code: this.item.item_code,
|
item_code: this.item.item_code,
|
||||||
name: this.bundle,
|
name: this.bundle,
|
||||||
voucher_no: this.item.parent,
|
voucher_no: !this.frm.is_new() ? this.item.parent : "",
|
||||||
}
|
}
|
||||||
}).then(r => {
|
}).then(r => {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
|
|||||||
@@ -112,9 +112,9 @@ def create_transaction(doctype, company, start_date):
|
|||||||
warehouse = get_warehouse(company)
|
warehouse = get_warehouse(company)
|
||||||
|
|
||||||
if document_type == "Purchase Order":
|
if document_type == "Purchase Order":
|
||||||
posting_date = get_random_date(start_date, 1, 30)
|
posting_date = get_random_date(start_date, 1, 25)
|
||||||
else:
|
else:
|
||||||
posting_date = get_random_date(start_date, 31, 364)
|
posting_date = get_random_date(start_date, 31, 350)
|
||||||
|
|
||||||
doctype.update(
|
doctype.update(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -301,7 +301,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Delivery Note",
|
"options": "Delivery Note",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
@@ -1401,7 +1402,7 @@
|
|||||||
"idx": 146,
|
"idx": 146,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-04 14:15:28.363184",
|
"modified": "2023-12-18 17:19:39.368239",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note",
|
"name": "Delivery Note",
|
||||||
|
|||||||
@@ -1294,7 +1294,3 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
|
|
||||||
def on_doctype_update():
|
|
||||||
frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"])
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
"""Drop unused return_against index"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
frappe.db.sql_ddl(
|
||||||
|
"ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`"
|
||||||
|
)
|
||||||
|
frappe.db.sql_ddl(
|
||||||
|
"ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error("Failed to drop unused index")
|
||||||
@@ -174,6 +174,115 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
for field, value in field_values.items():
|
for field, value in field_values.items():
|
||||||
self.assertEqual(cstr(serial_no.get(field)), value)
|
self.assertEqual(cstr(serial_no.get(field)), value)
|
||||||
|
|
||||||
|
def test_delivery_note_return_against_denormalized_serial_no(self):
|
||||||
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||||
|
sn_item = "Old Serial NO Item Return Test - 1"
|
||||||
|
make_item(
|
||||||
|
sn_item,
|
||||||
|
{
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "OSN-.####",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.flags.ignore_serial_batch_bundle_validation = True
|
||||||
|
serial_nos = [
|
||||||
|
"OSN-1",
|
||||||
|
"OSN-2",
|
||||||
|
"OSN-3",
|
||||||
|
"OSN-4",
|
||||||
|
"OSN-5",
|
||||||
|
"OSN-6",
|
||||||
|
"OSN-7",
|
||||||
|
"OSN-8",
|
||||||
|
"OSN-9",
|
||||||
|
"OSN-10",
|
||||||
|
"OSN-11",
|
||||||
|
"OSN-12",
|
||||||
|
]
|
||||||
|
|
||||||
|
for sn in serial_nos:
|
||||||
|
if not frappe.db.exists("Serial No", sn):
|
||||||
|
sn_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Serial No",
|
||||||
|
"item_code": sn_item,
|
||||||
|
"serial_no": sn,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
sn_doc.insert()
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||||
|
se_doc = make_stock_entry(
|
||||||
|
item_code=sn_item,
|
||||||
|
company=company,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=12,
|
||||||
|
basic_rate=100,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
se_doc.items[0].serial_no = "\n".join(serial_nos)
|
||||||
|
se_doc.submit()
|
||||||
|
|
||||||
|
self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=sn_item,
|
||||||
|
qty=12,
|
||||||
|
rate=500,
|
||||||
|
warehouse=warehouse,
|
||||||
|
company=company,
|
||||||
|
expense_account="Cost of Goods Sold - _TC",
|
||||||
|
cost_center="Main - _TC",
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
dn.items[0].serial_no = "\n".join(serial_nos)
|
||||||
|
dn.submit()
|
||||||
|
dn.reload()
|
||||||
|
|
||||||
|
self.assertTrue(dn.items[0].serial_no)
|
||||||
|
|
||||||
|
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||||
|
|
||||||
|
# return entry
|
||||||
|
dn1 = make_sales_return(dn.name)
|
||||||
|
|
||||||
|
dn1.items[0].qty = -2
|
||||||
|
|
||||||
|
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle)
|
||||||
|
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||||
|
bundle_doc.save()
|
||||||
|
|
||||||
|
dn1.save()
|
||||||
|
dn1.submit()
|
||||||
|
|
||||||
|
returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
|
||||||
|
for serial_no in returned_serial_nos1:
|
||||||
|
self.assertTrue(serial_no in serial_nos)
|
||||||
|
|
||||||
|
dn2 = make_sales_return(dn.name)
|
||||||
|
|
||||||
|
dn2.items[0].qty = -2
|
||||||
|
|
||||||
|
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle)
|
||||||
|
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||||
|
bundle_doc.save()
|
||||||
|
|
||||||
|
dn2.save()
|
||||||
|
dn2.submit()
|
||||||
|
|
||||||
|
returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
|
||||||
|
for serial_no in returned_serial_nos2:
|
||||||
|
self.assertTrue(serial_no in serial_nos)
|
||||||
|
self.assertFalse(serial_no in returned_serial_nos1)
|
||||||
|
|
||||||
def test_sales_return_for_non_bundled_items_partial(self):
|
def test_sales_return_for_non_bundled_items_partial(self):
|
||||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||||
|
|
||||||
@@ -1266,6 +1375,56 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
dn.reload()
|
dn.reload()
|
||||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||||
|
|
||||||
|
def test_sales_return_valuation_for_moving_average(self):
|
||||||
|
item_code = make_item(
|
||||||
|
"_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"}
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=5,
|
||||||
|
basic_rate=100.0,
|
||||||
|
posting_date=add_days(nowdate(), -5),
|
||||||
|
)
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4)
|
||||||
|
)
|
||||||
|
self.assertEqual(dn.items[0].incoming_rate, 100.0)
|
||||||
|
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=5,
|
||||||
|
basic_rate=200.0,
|
||||||
|
posting_date=add_days(nowdate(), -3),
|
||||||
|
)
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=5,
|
||||||
|
basic_rate=300.0,
|
||||||
|
posting_date=add_days(nowdate(), -2),
|
||||||
|
)
|
||||||
|
|
||||||
|
dn1 = create_delivery_note(
|
||||||
|
is_return=1,
|
||||||
|
item_code=item_code,
|
||||||
|
return_against=dn.name,
|
||||||
|
qty=-5,
|
||||||
|
rate=500,
|
||||||
|
company=dn.company,
|
||||||
|
expense_account="Cost of Goods Sold - _TC",
|
||||||
|
cost_center="Main - _TC",
|
||||||
|
do_not_submit=1,
|
||||||
|
posting_date=add_days(nowdate(), -1),
|
||||||
|
)
|
||||||
|
|
||||||
|
# (300 * 5) + (200 * 5) = 2500
|
||||||
|
# 2500 / 10 = 250
|
||||||
|
|
||||||
|
self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0)
|
||||||
|
|
||||||
|
|
||||||
def create_delivery_note(**args):
|
def create_delivery_note(**args):
|
||||||
dn = frappe.new_doc("Delivery Note")
|
dn = frappe.new_doc("Delivery Note")
|
||||||
|
|||||||
@@ -522,39 +522,25 @@ class TestItem(FrappeTestCase):
|
|||||||
self.assertEqual(factor, 1.0)
|
self.assertEqual(factor, 1.0)
|
||||||
|
|
||||||
def test_item_variant_by_manufacturer(self):
|
def test_item_variant_by_manufacturer(self):
|
||||||
fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}]
|
template = make_item(
|
||||||
set_item_variant_settings(fields)
|
"_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"}
|
||||||
|
).name
|
||||||
|
|
||||||
if frappe.db.exists("Item", "_Test Variant Mfg"):
|
for manufacturer in ["DFSS", "DASA", "ASAAS"]:
|
||||||
frappe.delete_doc("Item", "_Test Variant Mfg")
|
if not frappe.db.exists("Manufacturer", manufacturer):
|
||||||
if frappe.db.exists("Item", "_Test Variant Mfg-1"):
|
m_doc = frappe.new_doc("Manufacturer")
|
||||||
frappe.delete_doc("Item", "_Test Variant Mfg-1")
|
m_doc.short_name = manufacturer
|
||||||
if frappe.db.exists("Manufacturer", "MSG1"):
|
m_doc.insert()
|
||||||
frappe.delete_doc("Manufacturer", "MSG1")
|
|
||||||
|
|
||||||
template = frappe.get_doc(
|
self.assertFalse(frappe.db.exists("Item Manufacturer", {"manufacturer": "DFSS"}))
|
||||||
dict(
|
variant = get_variant(template, manufacturer="DFSS", manufacturer_part_no="DFSS-123")
|
||||||
doctype="Item",
|
|
||||||
item_code="_Test Variant Mfg",
|
|
||||||
has_variant=1,
|
|
||||||
item_group="Products",
|
|
||||||
variant_based_on="Manufacturer",
|
|
||||||
)
|
|
||||||
).insert()
|
|
||||||
|
|
||||||
manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert()
|
item_manufacturer = frappe.db.exists(
|
||||||
|
"Item Manufacturer", {"manufacturer": "DFSS", "item_code": variant.name}
|
||||||
|
)
|
||||||
|
self.assertTrue(item_manufacturer)
|
||||||
|
|
||||||
variant = get_variant(template.name, manufacturer=manufacturer.name)
|
frappe.delete_doc("Item Manufacturer", item_manufacturer)
|
||||||
self.assertEqual(variant.item_code, "_Test Variant Mfg-1")
|
|
||||||
self.assertEqual(variant.description, "_Test Variant Mfg")
|
|
||||||
self.assertEqual(variant.manufacturer, "MSG1")
|
|
||||||
variant.insert()
|
|
||||||
|
|
||||||
variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007")
|
|
||||||
self.assertEqual(variant.item_code, "_Test Variant Mfg-2")
|
|
||||||
self.assertEqual(variant.description, "_Test Variant Mfg")
|
|
||||||
self.assertEqual(variant.manufacturer, "MSG1")
|
|
||||||
self.assertEqual(variant.manufacturer_part_no, "007")
|
|
||||||
|
|
||||||
def test_stock_exists_against_template_item(self):
|
def test_stock_exists_against_template_item(self):
|
||||||
stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1)
|
stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1)
|
||||||
|
|||||||
@@ -199,9 +199,8 @@ frappe.ui.form.on('Material Request', {
|
|||||||
|
|
||||||
get_item_data: function(frm, item, overwrite_warehouse=false) {
|
get_item_data: function(frm, item, overwrite_warehouse=false) {
|
||||||
if (item && !item.item_code) { return; }
|
if (item && !item.item_code) { return; }
|
||||||
frm.call({
|
frappe.call({
|
||||||
method: "erpnext.stock.get_item_details.get_item_details",
|
method: "erpnext.stock.get_item_details.get_item_details",
|
||||||
child: item,
|
|
||||||
args: {
|
args: {
|
||||||
args: {
|
args: {
|
||||||
item_code: item.item_code,
|
item_code: item.item_code,
|
||||||
|
|||||||
@@ -169,7 +169,9 @@ class MaterialRequest(BuyingController):
|
|||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_requested_qty_in_production_plan()
|
self.update_requested_qty_in_production_plan()
|
||||||
self.update_requested_qty()
|
self.update_requested_qty()
|
||||||
if self.material_request_type == "Purchase":
|
if self.material_request_type == "Purchase" and frappe.db.exists(
|
||||||
|
"Budget", {"applicable_on_material_request": 1, "docstatus": 1}
|
||||||
|
):
|
||||||
self.validate_budget()
|
self.validate_budget()
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
|
|||||||
@@ -289,7 +289,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Purchase Receipt",
|
"options": "Purchase Receipt",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_addresses",
|
"fieldname": "section_addresses",
|
||||||
@@ -1251,7 +1252,7 @@
|
|||||||
"idx": 261,
|
"idx": 261,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-28 13:14:15.243474",
|
"modified": "2023-12-18 17:26:41.279663",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Purchase Receipt",
|
"name": "Purchase Receipt",
|
||||||
|
|||||||
@@ -1357,10 +1357,6 @@ def get_item_account_wise_additional_cost(purchase_document):
|
|||||||
return item_account_wise_cost
|
return item_account_wise_cost
|
||||||
|
|
||||||
|
|
||||||
def on_doctype_update():
|
|
||||||
frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
|
|
||||||
|
|
||||||
|
|
||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def update_regional_gl_entries(gl_list, doc):
|
def update_regional_gl_entries(gl_list, doc):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ from frappe.utils import (
|
|||||||
)
|
)
|
||||||
from frappe.utils.csvutils import build_csv_response
|
from frappe.utils.csvutils import build_csv_response
|
||||||
|
|
||||||
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
|
from erpnext.stock.serial_batch_bundle import (
|
||||||
|
BatchNoValuation,
|
||||||
|
SerialNoValuation,
|
||||||
|
get_batches_from_bundle,
|
||||||
|
)
|
||||||
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
|
||||||
|
|
||||||
|
|
||||||
@@ -123,6 +127,11 @@ class SerialandBatchBundle(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_serial_nos_duplicate(self):
|
def validate_serial_nos_duplicate(self):
|
||||||
|
# Don't inward same serial number multiple times
|
||||||
|
|
||||||
|
if not self.warehouse:
|
||||||
|
return
|
||||||
|
|
||||||
if self.voucher_type in ["Stock Reconciliation", "Stock Entry"] and self.docstatus != 1:
|
if self.voucher_type in ["Stock Reconciliation", "Stock Entry"] and self.docstatus != 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -146,7 +155,6 @@ class SerialandBatchBundle(Document):
|
|||||||
kwargs["voucher_no"] = self.voucher_no
|
kwargs["voucher_no"] = self.voucher_no
|
||||||
|
|
||||||
available_serial_nos = get_available_serial_nos(kwargs)
|
available_serial_nos = get_available_serial_nos(kwargs)
|
||||||
|
|
||||||
for data in available_serial_nos:
|
for data in available_serial_nos:
|
||||||
if data.serial_no in serial_nos:
|
if data.serial_no in serial_nos:
|
||||||
self.throw_error_message(
|
self.throw_error_message(
|
||||||
@@ -157,7 +165,7 @@ class SerialandBatchBundle(Document):
|
|||||||
def throw_error_message(self, message, exception=frappe.ValidationError):
|
def throw_error_message(self, message, exception=frappe.ValidationError):
|
||||||
frappe.throw(_(message), exception, title=_("Error"))
|
frappe.throw(_(message), exception, title=_("Error"))
|
||||||
|
|
||||||
def set_incoming_rate(self, row=None, save=False):
|
def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False):
|
||||||
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
||||||
"Installation Note",
|
"Installation Note",
|
||||||
"Job Card",
|
"Job Card",
|
||||||
@@ -167,7 +175,9 @@ class SerialandBatchBundle(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.type_of_transaction == "Outward":
|
if self.type_of_transaction == "Outward":
|
||||||
self.set_incoming_rate_for_outward_transaction(row, save)
|
self.set_incoming_rate_for_outward_transaction(
|
||||||
|
row, save, allow_negative_stock=allow_negative_stock
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.set_incoming_rate_for_inward_transaction(row, save)
|
self.set_incoming_rate_for_inward_transaction(row, save)
|
||||||
|
|
||||||
@@ -188,7 +198,9 @@ class SerialandBatchBundle(Document):
|
|||||||
def get_serial_nos(self):
|
def get_serial_nos(self):
|
||||||
return [d.serial_no for d in self.entries if d.serial_no]
|
return [d.serial_no for d in self.entries if d.serial_no]
|
||||||
|
|
||||||
def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
|
def set_incoming_rate_for_outward_transaction(
|
||||||
|
self, row=None, save=False, allow_negative_stock=False
|
||||||
|
):
|
||||||
sle = self.get_sle_for_outward_transaction()
|
sle = self.get_sle_for_outward_transaction()
|
||||||
|
|
||||||
if self.has_serial_no:
|
if self.has_serial_no:
|
||||||
@@ -217,7 +229,8 @@ class SerialandBatchBundle(Document):
|
|||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
available_qty += flt(d.qty)
|
available_qty += flt(d.qty)
|
||||||
|
|
||||||
self.validate_negative_batch(d.batch_no, available_qty)
|
if not allow_negative_stock:
|
||||||
|
self.validate_negative_batch(d.batch_no, available_qty)
|
||||||
|
|
||||||
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
|
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
|
||||||
|
|
||||||
@@ -322,6 +335,19 @@ class SerialandBatchBundle(Document):
|
|||||||
):
|
):
|
||||||
values_to_set["posting_time"] = parent.posting_time
|
values_to_set["posting_time"] = parent.posting_time
|
||||||
|
|
||||||
|
if parent.doctype in [
|
||||||
|
"Delivery Note",
|
||||||
|
"Purchase Receipt",
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Sales Invoice",
|
||||||
|
] and parent.get("is_return"):
|
||||||
|
return_ref_field = frappe.scrub(parent.doctype) + "_item"
|
||||||
|
if parent.doctype == "Delivery Note":
|
||||||
|
return_ref_field = "dn_detail"
|
||||||
|
|
||||||
|
if row.get(return_ref_field):
|
||||||
|
values_to_set["returned_against"] = row.get(return_ref_field)
|
||||||
|
|
||||||
if values_to_set:
|
if values_to_set:
|
||||||
self.db_set(values_to_set)
|
self.db_set(values_to_set)
|
||||||
|
|
||||||
@@ -433,7 +459,7 @@ class SerialandBatchBundle(Document):
|
|||||||
qty_field = "qty"
|
qty_field = "qty"
|
||||||
|
|
||||||
precision = row.precision
|
precision = row.precision
|
||||||
if self.voucher_type in ["Subcontracting Receipt"]:
|
if row.get("doctype") in ["Subcontracting Receipt Supplied Item"]:
|
||||||
qty_field = "consumed_qty"
|
qty_field = "consumed_qty"
|
||||||
|
|
||||||
if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01:
|
if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01:
|
||||||
@@ -504,7 +530,6 @@ class SerialandBatchBundle(Document):
|
|||||||
batch_nos = []
|
batch_nos = []
|
||||||
|
|
||||||
serial_batches = {}
|
serial_batches = {}
|
||||||
|
|
||||||
for row in self.entries:
|
for row in self.entries:
|
||||||
if self.has_serial_no and not row.serial_no:
|
if self.has_serial_no and not row.serial_no:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
@@ -585,6 +610,67 @@ class SerialandBatchBundle(Document):
|
|||||||
f"Batch Nos {bold(incorrect_batch_nos)} does not belong to Item {bold(self.item_code)}"
|
f"Batch Nos {bold(incorrect_batch_nos)} does not belong to Item {bold(self.item_code)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_serial_and_batch_no_for_returned(self):
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
if not self.returned_against:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.voucher_type not in [
|
||||||
|
"Purchase Receipt",
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Sales Invoice",
|
||||||
|
"Delivery Note",
|
||||||
|
]:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = self.get_orignal_document_data()
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
serial_nos, batches = [], []
|
||||||
|
current_serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||||
|
current_batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
if self.has_serial_no:
|
||||||
|
if d.serial_and_batch_bundle:
|
||||||
|
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle)
|
||||||
|
else:
|
||||||
|
serial_nos = get_serial_nos(d.serial_no)
|
||||||
|
|
||||||
|
elif self.has_batch_no:
|
||||||
|
if d.serial_and_batch_bundle:
|
||||||
|
batches = get_batches_from_bundle(d.serial_and_batch_bundle)
|
||||||
|
else:
|
||||||
|
batches = frappe._dict({d.batch_no: d.stock_qty})
|
||||||
|
|
||||||
|
if batches:
|
||||||
|
batches = [d for d in batches if batches[d] > 0]
|
||||||
|
|
||||||
|
if serial_nos:
|
||||||
|
if not set(current_serial_nos).issubset(set(serial_nos)):
|
||||||
|
self.throw_error_message(
|
||||||
|
f"Serial Nos {bold(', '.join(serial_nos))} are not part of the original document."
|
||||||
|
)
|
||||||
|
|
||||||
|
if batches:
|
||||||
|
if not set(current_batches).issubset(set(batches)):
|
||||||
|
self.throw_error_message(
|
||||||
|
f"Batch Nos {bold(', '.join(batches))} are not part of the original document."
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_orignal_document_data(self):
|
||||||
|
fields = ["serial_and_batch_bundle", "stock_qty"]
|
||||||
|
if self.has_serial_no:
|
||||||
|
fields.append("serial_no")
|
||||||
|
|
||||||
|
elif self.has_batch_no:
|
||||||
|
fields.append("batch_no")
|
||||||
|
|
||||||
|
child_doc = self.voucher_type + " Item"
|
||||||
|
return frappe.get_all(child_doc, fields=fields, filters={"name": self.returned_against})
|
||||||
|
|
||||||
def validate_duplicate_serial_and_batch_no(self):
|
def validate_duplicate_serial_and_batch_no(self):
|
||||||
serial_nos = []
|
serial_nos = []
|
||||||
batch_nos = []
|
batch_nos = []
|
||||||
@@ -683,9 +769,29 @@ class SerialandBatchBundle(Document):
|
|||||||
for batch in batches:
|
for batch in batches:
|
||||||
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
|
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
|
||||||
|
|
||||||
|
def before_submit(self):
|
||||||
|
self.validate_serial_and_batch_no_for_returned()
|
||||||
|
self.set_purchase_document_no()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_serial_nos_inventory()
|
self.validate_serial_nos_inventory()
|
||||||
|
|
||||||
|
def set_purchase_document_no(self):
|
||||||
|
if not self.has_serial_no:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.total_qty > 0:
|
||||||
|
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||||
|
sn_table = frappe.qb.DocType("Serial No")
|
||||||
|
(
|
||||||
|
frappe.qb.update(sn_table)
|
||||||
|
.set(
|
||||||
|
sn_table.purchase_document_no,
|
||||||
|
self.voucher_no if not sn_table.purchase_document_no else self.voucher_no,
|
||||||
|
)
|
||||||
|
.where(sn_table.name.isin(serial_nos))
|
||||||
|
).run()
|
||||||
|
|
||||||
def validate_serial_and_batch_inventory(self):
|
def validate_serial_and_batch_inventory(self):
|
||||||
self.check_future_entries_exists()
|
self.check_future_entries_exists()
|
||||||
self.validate_batch_inventory()
|
self.validate_batch_inventory()
|
||||||
|
|||||||
@@ -27,8 +27,6 @@
|
|||||||
"column_break_24",
|
"column_break_24",
|
||||||
"location",
|
"location",
|
||||||
"employee",
|
"employee",
|
||||||
"delivery_details",
|
|
||||||
"delivery_document_type",
|
|
||||||
"warranty_amc_details",
|
"warranty_amc_details",
|
||||||
"column_break6",
|
"column_break6",
|
||||||
"warranty_expiry_date",
|
"warranty_expiry_date",
|
||||||
@@ -39,7 +37,8 @@
|
|||||||
"more_info",
|
"more_info",
|
||||||
"company",
|
"company",
|
||||||
"column_break_2cmm",
|
"column_break_2cmm",
|
||||||
"work_order"
|
"work_order",
|
||||||
|
"purchase_document_no"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -153,20 +152,6 @@
|
|||||||
"options": "Employee",
|
"options": "Employee",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "delivery_details",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Delivery Details",
|
|
||||||
"oldfieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "delivery_document_type",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Delivery Document Type",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "DocType",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "warranty_amc_details",
|
"fieldname": "warranty_amc_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@@ -275,12 +260,19 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_2cmm",
|
"fieldname": "column_break_2cmm",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "purchase_document_no",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Creation Document No",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-barcode",
|
"icon": "fa fa-barcode",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-28 15:37:59.489945",
|
"modified": "2023-12-17 10:52:55.767839",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Serial No",
|
"name": "Serial No",
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class SerialNo(StockController):
|
|||||||
batch_no: DF.Link | None
|
batch_no: DF.Link | None
|
||||||
brand: DF.Link | None
|
brand: DF.Link | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
delivery_document_type: DF.Link | None
|
|
||||||
description: DF.Text | None
|
description: DF.Text | None
|
||||||
employee: DF.Link | None
|
employee: DF.Link | None
|
||||||
item_code: DF.Link
|
item_code: DF.Link
|
||||||
@@ -51,6 +50,7 @@ class SerialNo(StockController):
|
|||||||
maintenance_status: DF.Literal[
|
maintenance_status: DF.Literal[
|
||||||
"", "Under Warranty", "Out of Warranty", "Under AMC", "Out of AMC"
|
"", "Under Warranty", "Out of Warranty", "Under AMC", "Out of AMC"
|
||||||
]
|
]
|
||||||
|
purchase_document_no: DF.Data | None
|
||||||
purchase_rate: DF.Float
|
purchase_rate: DF.Float
|
||||||
serial_no: DF.Data
|
serial_no: DF.Data
|
||||||
status: DF.Literal["", "Active", "Inactive", "Delivered", "Expired"]
|
status: DF.Literal["", "Active", "Inactive", "Delivered", "Expired"]
|
||||||
@@ -231,26 +231,6 @@ def auto_fetch_serial_number(
|
|||||||
return sorted([d.get("name") for d in serial_numbers])
|
return sorted([d.get("name") for d in serial_numbers])
|
||||||
|
|
||||||
|
|
||||||
def get_delivered_serial_nos(serial_nos):
|
|
||||||
"""
|
|
||||||
Returns serial numbers that delivered from the list of serial numbers
|
|
||||||
"""
|
|
||||||
from frappe.query_builder.functions import Coalesce
|
|
||||||
|
|
||||||
SerialNo = frappe.qb.DocType("Serial No")
|
|
||||||
serial_nos = get_serial_nos(serial_nos)
|
|
||||||
query = (
|
|
||||||
frappe.qb.select(SerialNo.name)
|
|
||||||
.from_(SerialNo)
|
|
||||||
.where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != ""))
|
|
||||||
)
|
|
||||||
|
|
||||||
result = query.run()
|
|
||||||
if result and len(result) > 0:
|
|
||||||
delivered_serial_nos = [row[0] for row in result]
|
|
||||||
return delivered_serial_nos
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_pos_reserved_serial_nos(filters):
|
def get_pos_reserved_serial_nos(filters):
|
||||||
if isinstance(filters, str):
|
if isinstance(filters, str):
|
||||||
|
|||||||
@@ -781,10 +781,9 @@ frappe.ui.form.on('Stock Entry Detail', {
|
|||||||
});
|
});
|
||||||
refresh_field("items");
|
refresh_field("items");
|
||||||
|
|
||||||
let no_batch_serial_number_value = !d.serial_no;
|
let no_batch_serial_number_value = false;
|
||||||
if (d.has_batch_no && !d.has_serial_no) {
|
if (d.has_serial_no || d.has_batch_no) {
|
||||||
// check only batch_no for batched item
|
no_batch_serial_number_value = true;
|
||||||
no_batch_serial_number_value = !d.batch_no;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
|
if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
|
||||||
@@ -941,6 +940,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
|||||||
}
|
}
|
||||||
|
|
||||||
scan_barcode() {
|
scan_barcode() {
|
||||||
|
frappe.flags.dialog_set = false;
|
||||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
||||||
barcode_scanner.process_scan();
|
barcode_scanner.process_scan();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ class StockLedgerEntry(Document):
|
|||||||
frappe.throw(_("Actual Qty is mandatory"))
|
frappe.throw(_("Actual Qty is mandatory"))
|
||||||
|
|
||||||
def validate_serial_batch_no_bundle(self):
|
def validate_serial_batch_no_bundle(self):
|
||||||
|
if self.is_cancelled == 1:
|
||||||
|
return
|
||||||
|
|
||||||
item_detail = frappe.get_cached_value(
|
item_detail = frappe.get_cached_value(
|
||||||
"Item",
|
"Item",
|
||||||
self.item_code,
|
self.item_code,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Optional
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold, msgprint
|
from frappe import _, bold, msgprint
|
||||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||||
from frappe.utils import cint, cstr, flt
|
from frappe.utils import add_to_date, cint, cstr, flt
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.utils import get_company_default
|
from erpnext.accounts.utils import get_company_default
|
||||||
@@ -116,9 +116,12 @@ class StockReconciliation(StockController):
|
|||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
self.delete_auto_created_batches()
|
self.delete_auto_created_batches()
|
||||||
|
|
||||||
def set_current_serial_and_batch_bundle(self):
|
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
|
||||||
"""Set Serial and Batch Bundle for each item"""
|
"""Set Serial and Batch Bundle for each item"""
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
|
if voucher_detail_no and voucher_detail_no != item.name:
|
||||||
|
continue
|
||||||
|
|
||||||
item_details = frappe.get_cached_value(
|
item_details = frappe.get_cached_value(
|
||||||
"Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
"Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||||
)
|
)
|
||||||
@@ -176,6 +179,7 @@ class StockReconciliation(StockController):
|
|||||||
"warehouse": item.warehouse,
|
"warehouse": item.warehouse,
|
||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
|
"ignore_voucher_nos": [self.name],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -191,11 +195,36 @@ class StockReconciliation(StockController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not serial_and_batch_bundle.entries:
|
if not serial_and_batch_bundle.entries:
|
||||||
|
if voucher_detail_no:
|
||||||
|
return
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item.current_serial_and_batch_bundle = serial_and_batch_bundle.save().name
|
serial_and_batch_bundle.save()
|
||||||
|
item.current_serial_and_batch_bundle = serial_and_batch_bundle.name
|
||||||
item.current_qty = abs(serial_and_batch_bundle.total_qty)
|
item.current_qty = abs(serial_and_batch_bundle.total_qty)
|
||||||
item.current_valuation_rate = abs(serial_and_batch_bundle.avg_rate)
|
item.current_valuation_rate = abs(serial_and_batch_bundle.avg_rate)
|
||||||
|
if save:
|
||||||
|
sle_creation = frappe.db.get_value(
|
||||||
|
"Serial and Batch Bundle", item.serial_and_batch_bundle, "creation"
|
||||||
|
)
|
||||||
|
creation = add_to_date(sle_creation, seconds=-1)
|
||||||
|
item.db_set(
|
||||||
|
{
|
||||||
|
"current_serial_and_batch_bundle": item.current_serial_and_batch_bundle,
|
||||||
|
"current_qty": item.current_qty,
|
||||||
|
"current_valuation_rate": item.current_valuation_rate,
|
||||||
|
"creation": creation,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_and_batch_bundle.db_set(
|
||||||
|
{
|
||||||
|
"creation": creation,
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"voucher_detail_no": voucher_detail_no,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def set_new_serial_and_batch_bundle(self):
|
def set_new_serial_and_batch_bundle(self):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
@@ -737,56 +766,84 @@ class StockReconciliation(StockController):
|
|||||||
else:
|
else:
|
||||||
self._cancel()
|
self._cancel()
|
||||||
|
|
||||||
def recalculate_current_qty(self, item_code, batch_no):
|
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
|
||||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||||
|
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
|
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
if (
|
if voucher_detail_no != row.name:
|
||||||
not (row.item_code == item_code and row.batch_no == batch_no)
|
|
||||||
and not row.serial_and_batch_bundle
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
current_qty = 0.0
|
||||||
if row.current_serial_and_batch_bundle:
|
if row.current_serial_and_batch_bundle:
|
||||||
self.recalculate_qty_for_serial_and_batch_bundle(row)
|
current_qty = self.get_qty_for_serial_and_batch_bundle(row)
|
||||||
continue
|
elif row.batch_no:
|
||||||
|
current_qty = get_batch_qty_for_stock_reco(
|
||||||
current_qty = get_batch_qty_for_stock_reco(
|
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
||||||
item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name
|
)
|
||||||
)
|
|
||||||
|
|
||||||
precesion = row.precision("current_qty")
|
precesion = row.precision("current_qty")
|
||||||
if flt(current_qty, precesion) == flt(row.current_qty, precesion):
|
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||||
continue
|
val_rate = get_valuation_rate(
|
||||||
|
row.item_code,
|
||||||
|
row.warehouse,
|
||||||
|
self.doctype,
|
||||||
|
self.name,
|
||||||
|
company=self.company,
|
||||||
|
batch_no=row.batch_no,
|
||||||
|
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||||
|
)
|
||||||
|
|
||||||
val_rate = get_valuation_rate(
|
row.current_valuation_rate = val_rate
|
||||||
item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no
|
row.current_qty = current_qty
|
||||||
)
|
row.db_set(
|
||||||
|
{
|
||||||
|
"current_qty": row.current_qty,
|
||||||
|
"current_valuation_rate": row.current_valuation_rate,
|
||||||
|
"current_amount": flt(row.current_qty * row.current_valuation_rate),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
row.current_valuation_rate = val_rate
|
if (
|
||||||
if not row.current_qty and current_qty:
|
add_new_sle
|
||||||
sle = self.get_sle_for_items(row)
|
and not frappe.db.get_value(
|
||||||
sle.actual_qty = current_qty * -1
|
"Stock Ledger Entry",
|
||||||
sle.valuation_rate = val_rate
|
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
|
||||||
sl_entries.append(sle)
|
"name",
|
||||||
|
)
|
||||||
|
and (not row.current_serial_and_batch_bundle and not row.batch_no)
|
||||||
|
):
|
||||||
|
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
|
||||||
|
row.reload()
|
||||||
|
|
||||||
row.current_qty = current_qty
|
if row.current_qty > 0 and row.current_serial_and_batch_bundle:
|
||||||
row.db_set(
|
new_sle = self.get_sle_for_items(row)
|
||||||
{
|
new_sle.actual_qty = row.current_qty * -1
|
||||||
"current_qty": row.current_qty,
|
new_sle.valuation_rate = row.current_valuation_rate
|
||||||
"current_valuation_rate": row.current_valuation_rate,
|
new_sle.creation_time = add_to_date(sle_creation, seconds=-1)
|
||||||
"current_amount": flt(row.current_qty * row.current_valuation_rate),
|
new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
|
||||||
}
|
new_sle.qty_after_transaction = 0.0
|
||||||
)
|
sl_entries.append(new_sle)
|
||||||
|
|
||||||
if sl_entries:
|
if sl_entries:
|
||||||
self.make_sl_entries(sl_entries, allow_negative_stock=True)
|
self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed())
|
||||||
|
if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}):
|
||||||
|
self.repost_future_sle_and_gle(force=True)
|
||||||
|
|
||||||
def recalculate_qty_for_serial_and_batch_bundle(self, row):
|
def has_negative_stock_allowed(self):
|
||||||
|
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
|
||||||
|
|
||||||
|
if all(d.serial_and_batch_bundle and flt(d.qty) == flt(d.current_qty) for d in self.items):
|
||||||
|
allow_negative_stock = True
|
||||||
|
|
||||||
|
return allow_negative_stock
|
||||||
|
|
||||||
|
def get_qty_for_serial_and_batch_bundle(self, row):
|
||||||
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
|
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
|
||||||
precision = doc.entries[0].precision("qty")
|
precision = doc.entries[0].precision("qty")
|
||||||
|
|
||||||
|
current_qty = 0
|
||||||
for d in doc.entries:
|
for d in doc.entries:
|
||||||
qty = (
|
qty = (
|
||||||
get_batch_qty(
|
get_batch_qty(
|
||||||
@@ -799,10 +856,12 @@ class StockReconciliation(StockController):
|
|||||||
or 0
|
or 0
|
||||||
) * -1
|
) * -1
|
||||||
|
|
||||||
if flt(d.qty, precision) == flt(qty, precision):
|
if flt(d.qty, precision) != flt(qty, precision):
|
||||||
continue
|
d.db_set("qty", qty)
|
||||||
|
|
||||||
d.db_set("qty", qty)
|
current_qty += qty
|
||||||
|
|
||||||
|
return abs(current_qty)
|
||||||
|
|
||||||
|
|
||||||
def get_batch_qty_for_stock_reco(
|
def get_batch_qty_for_stock_reco(
|
||||||
|
|||||||
@@ -742,13 +742,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
se2.cancel()
|
se2.cancel()
|
||||||
|
|
||||||
self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"),
|
|
||||||
"Completed",
|
|
||||||
)
|
|
||||||
|
|
||||||
sle = frappe.get_all(
|
sle = frappe.get_all(
|
||||||
"Stock Ledger Entry",
|
"Stock Ledger Entry",
|
||||||
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
|
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
|
||||||
@@ -766,6 +759,68 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
|
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
|
||||||
|
|
||||||
|
def test_backdated_stock_reco_entry_with_batch(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
item_code = self.make_item(
|
||||||
|
"Test New Batch Item ABCVSD",
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"batch_number_series": "BNS9.####",
|
||||||
|
"create_new_batch": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
|
# Stock Reco for 100, Balace Qty 100
|
||||||
|
stock_reco = create_stock_reconciliation(
|
||||||
|
item_code=item_code,
|
||||||
|
posting_date=nowdate(),
|
||||||
|
posting_time="11:00:00",
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=100,
|
||||||
|
rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
sles = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["actual_qty"],
|
||||||
|
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(sles), 1)
|
||||||
|
|
||||||
|
stock_reco.reload()
|
||||||
|
batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
# Stock Reco for 100, Balace Qty 100
|
||||||
|
stock_reco1 = create_stock_reconciliation(
|
||||||
|
item_code=item_code,
|
||||||
|
posting_date=add_days(nowdate(), -1),
|
||||||
|
posting_time="11:00:00",
|
||||||
|
batch_no=batch_no,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=60,
|
||||||
|
rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
sles = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["actual_qty"],
|
||||||
|
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_reco1.reload()
|
||||||
|
new_batch_no = get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
self.assertEqual(len(sles), 2)
|
||||||
|
|
||||||
|
for row in sles:
|
||||||
|
if row.actual_qty < 0:
|
||||||
|
self.assertEqual(row.actual_qty, -60)
|
||||||
|
|
||||||
def test_update_stock_reconciliation_while_reposting(self):
|
def test_update_stock_reconciliation_while_reposting(self):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
"fieldname": "current_serial_and_batch_bundle",
|
"fieldname": "current_serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Current Serial / Batch Bundle",
|
"label": "Current Serial / Batch Bundle",
|
||||||
|
"no_copy": 1,
|
||||||
"options": "Serial and Batch Bundle",
|
"options": "Serial and Batch Bundle",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -216,7 +217,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-26 12:54:34.011915",
|
"modified": "2023-11-02 15:47:07.929550",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Reconciliation Item",
|
"name": "Stock Reconciliation Item",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import frappe
|
|||||||
from frappe import _, throw
|
from frappe import _, throw
|
||||||
from frappe.model import child_table_fields, default_fields
|
from frappe.model import child_table_fields, default_fields
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
|
from frappe.model.utils import get_fetch_values
|
||||||
from frappe.query_builder.functions import IfNull, Sum
|
from frappe.query_builder.functions import IfNull, Sum
|
||||||
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
|
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
|
||||||
|
|
||||||
@@ -574,6 +575,9 @@ def get_item_tax_template(args, item, out):
|
|||||||
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
|
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
|
||||||
item_group = item_group_doc.parent_item_group
|
item_group = item_group_doc.parent_item_group
|
||||||
|
|
||||||
|
if args.get("child_doctype") and item_tax_template:
|
||||||
|
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
|
||||||
|
|
||||||
|
|
||||||
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||||
if out is None:
|
if out is None:
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ class StockBalanceReport(object):
|
|||||||
"fieldname": "bal_val",
|
"fieldname": "bal_val",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"options": "currency",
|
"options": "Company:company:default_currency",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Opening Qty"),
|
"label": _("Opening Qty"),
|
||||||
@@ -427,7 +427,7 @@ class StockBalanceReport(object):
|
|||||||
"fieldname": "opening_val",
|
"fieldname": "opening_val",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"width": 110,
|
"width": 110,
|
||||||
"options": "currency",
|
"options": "Company:company:default_currency",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("In Qty"),
|
"label": _("In Qty"),
|
||||||
|
|||||||
@@ -249,6 +249,13 @@ def get_columns(filters):
|
|||||||
"options": "Serial No",
|
"options": "Serial No",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": _("Serial and Batch Bundle"),
|
||||||
|
"fieldname": "serial_and_batch_bundle",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Serial and Batch Bundle",
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
|
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
|
||||||
{
|
{
|
||||||
"label": _("Project"),
|
"label": _("Project"),
|
||||||
@@ -287,6 +294,7 @@ def get_stock_ledger_entries(filters, items):
|
|||||||
sle.voucher_type,
|
sle.voucher_type,
|
||||||
sle.qty_after_transaction,
|
sle.qty_after_transaction,
|
||||||
sle.stock_value_difference,
|
sle.stock_value_difference,
|
||||||
|
sle.serial_and_batch_bundle,
|
||||||
sle.voucher_no,
|
sle.voucher_no,
|
||||||
sle.stock_value,
|
sle.stock_value,
|
||||||
sle.batch_no,
|
sle.batch_no,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ SLE_FIELDS = (
|
|||||||
"stock_value_difference",
|
"stock_value_difference",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
"voucher_detail_no",
|
"voucher_detail_no",
|
||||||
|
"serial_and_batch_bundle",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +65,11 @@ def add_invariant_check_fields(sles):
|
|||||||
|
|
||||||
balance_qty += sle.actual_qty
|
balance_qty += sle.actual_qty
|
||||||
balance_stock_value += sle.stock_value_difference
|
balance_stock_value += sle.stock_value_difference
|
||||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
if (
|
||||||
|
sle.voucher_type == "Stock Reconciliation"
|
||||||
|
and not sle.batch_no
|
||||||
|
and not sle.serial_and_batch_bundle
|
||||||
|
):
|
||||||
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
|
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
|
||||||
if balance_qty is None:
|
if balance_qty is None:
|
||||||
balance_qty = sle.qty_after_transaction
|
balance_qty = sle.qty_after_transaction
|
||||||
@@ -143,6 +148,12 @@ def get_columns():
|
|||||||
"label": _("Batch"),
|
"label": _("Batch"),
|
||||||
"options": "Batch",
|
"options": "Batch",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "serial_and_batch_bundle",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": _("Serial and Batch Bundle"),
|
||||||
|
"options": "Serial and Batch Bundle",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "use_batchwise_valuation",
|
"fieldname": "use_batchwise_valuation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
|||||||
@@ -218,15 +218,16 @@ class SerialBatchBundle:
|
|||||||
).validate_serial_and_batch_inventory()
|
).validate_serial_and_batch_inventory()
|
||||||
|
|
||||||
def post_process(self):
|
def post_process(self):
|
||||||
if not self.sle.serial_and_batch_bundle:
|
if not self.sle.serial_and_batch_bundle and not self.sle.serial_no and not self.sle.batch_no:
|
||||||
return
|
return
|
||||||
|
|
||||||
docstatus = frappe.get_cached_value(
|
if self.sle.serial_and_batch_bundle:
|
||||||
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
|
docstatus = frappe.get_cached_value(
|
||||||
)
|
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
|
||||||
|
)
|
||||||
|
|
||||||
if docstatus != 1:
|
if docstatus != 1:
|
||||||
self.submit_serial_and_batch_bundle()
|
self.submit_serial_and_batch_bundle()
|
||||||
|
|
||||||
if self.item_details.has_serial_no == 1:
|
if self.item_details.has_serial_no == 1:
|
||||||
self.set_warehouse_and_status_in_serial_nos()
|
self.set_warehouse_and_status_in_serial_nos()
|
||||||
@@ -249,7 +250,12 @@ class SerialBatchBundle:
|
|||||||
doc.submit()
|
doc.submit()
|
||||||
|
|
||||||
def set_warehouse_and_status_in_serial_nos(self):
|
def set_warehouse_and_status_in_serial_nos(self):
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
|
||||||
|
|
||||||
serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
|
serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
|
||||||
|
if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
|
||||||
|
serial_nos = get_parsed_serial_nos(self.sle.serial_no)
|
||||||
|
|
||||||
warehouse = self.warehouse if self.sle.actual_qty > 0 else None
|
warehouse = self.warehouse if self.sle.actual_qty > 0 else None
|
||||||
|
|
||||||
if not serial_nos:
|
if not serial_nos:
|
||||||
@@ -263,7 +269,14 @@ class SerialBatchBundle:
|
|||||||
(
|
(
|
||||||
frappe.qb.update(sn_table)
|
frappe.qb.update(sn_table)
|
||||||
.set(sn_table.warehouse, warehouse)
|
.set(sn_table.warehouse, warehouse)
|
||||||
.set(sn_table.status, "Active" if warehouse else status)
|
.set(
|
||||||
|
sn_table.status,
|
||||||
|
"Active"
|
||||||
|
if warehouse
|
||||||
|
else status
|
||||||
|
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
|
||||||
|
else "Inactive",
|
||||||
|
)
|
||||||
.where(sn_table.name.isin(serial_nos))
|
.where(sn_table.name.isin(serial_nos))
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
@@ -290,6 +303,8 @@ class SerialBatchBundle:
|
|||||||
from erpnext.stock.doctype.batch.batch import get_available_batches
|
from erpnext.stock.doctype.batch.batch import get_available_batches
|
||||||
|
|
||||||
batches = get_batch_nos(self.sle.serial_and_batch_bundle)
|
batches = get_batch_nos(self.sle.serial_and_batch_bundle)
|
||||||
|
if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
|
||||||
|
batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
|
||||||
|
|
||||||
batches_qty = get_available_batches(
|
batches_qty = get_available_batches(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
@@ -312,13 +327,35 @@ def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
|
|||||||
if serial_nos:
|
if serial_nos:
|
||||||
filters["serial_no"] = ("in", serial_nos)
|
filters["serial_no"] = ("in", serial_nos)
|
||||||
|
|
||||||
entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters)
|
entries = frappe.get_all(
|
||||||
|
"Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx"
|
||||||
|
)
|
||||||
if not entries:
|
if not entries:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return [d.serial_no for d in entries if d.serial_no]
|
return [d.serial_no for d in entries if d.serial_no]
|
||||||
|
|
||||||
|
|
||||||
|
def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
|
||||||
|
if not serial_and_batch_bundle:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")}
|
||||||
|
if isinstance(serial_and_batch_bundle, list):
|
||||||
|
filters = {"parent": ("in", serial_and_batch_bundle)}
|
||||||
|
|
||||||
|
if batches:
|
||||||
|
filters["batch_no"] = ("in", batches)
|
||||||
|
|
||||||
|
entries = frappe.get_all(
|
||||||
|
"Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1
|
||||||
|
)
|
||||||
|
if not entries:
|
||||||
|
return frappe._dict({})
|
||||||
|
|
||||||
|
return frappe._dict(entries)
|
||||||
|
|
||||||
|
|
||||||
def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
|
def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
|
||||||
return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
|
return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,11 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
|
|||||||
sle.allow_negative_stock = allow_negative_stock
|
sle.allow_negative_stock = allow_negative_stock
|
||||||
sle.via_landed_cost_voucher = via_landed_cost_voucher
|
sle.via_landed_cost_voucher = via_landed_cost_voucher
|
||||||
sle.submit()
|
sle.submit()
|
||||||
|
|
||||||
|
# Added to handle the case when the stock ledger entry is created from the repostig
|
||||||
|
if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
|
||||||
|
sle.db_set("creation", args.get("creation_time"))
|
||||||
|
|
||||||
return sle
|
return sle
|
||||||
|
|
||||||
|
|
||||||
@@ -696,9 +701,11 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
sle.voucher_type == "Stock Reconciliation"
|
sle.voucher_type == "Stock Reconciliation"
|
||||||
and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle))
|
and (
|
||||||
|
sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
|
||||||
|
)
|
||||||
and sle.voucher_detail_no
|
and sle.voucher_detail_no
|
||||||
and sle.actual_qty < 0
|
and not self.args.get("sle_id")
|
||||||
):
|
):
|
||||||
self.reset_actual_qty_for_stock_reco(sle)
|
self.reset_actual_qty_for_stock_reco(sle)
|
||||||
|
|
||||||
@@ -765,27 +772,22 @@ class update_entries_after(object):
|
|||||||
self.update_outgoing_rate_on_transaction(sle)
|
self.update_outgoing_rate_on_transaction(sle)
|
||||||
|
|
||||||
def reset_actual_qty_for_stock_reco(self, sle):
|
def reset_actual_qty_for_stock_reco(self, sle):
|
||||||
if sle.serial_and_batch_bundle:
|
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
||||||
current_qty = frappe.get_cached_value(
|
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||||
"Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty"
|
|
||||||
|
if sle.actual_qty < 0:
|
||||||
|
sle.actual_qty = (
|
||||||
|
flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
|
||||||
|
* -1
|
||||||
)
|
)
|
||||||
|
|
||||||
if current_qty is not None:
|
if abs(sle.actual_qty) == 0.0:
|
||||||
current_qty = abs(current_qty)
|
sle.is_cancelled = 1
|
||||||
else:
|
|
||||||
current_qty = frappe.get_cached_value(
|
|
||||||
"Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
|
|
||||||
)
|
|
||||||
|
|
||||||
if current_qty:
|
|
||||||
sle.actual_qty = current_qty * -1
|
|
||||||
elif current_qty == 0:
|
|
||||||
sle.is_cancelled = 1
|
|
||||||
|
|
||||||
def calculate_valuation_for_serial_batch_bundle(self, sle):
|
def calculate_valuation_for_serial_batch_bundle(self, sle):
|
||||||
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||||
|
|
||||||
doc.set_incoming_rate(save=True)
|
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
|
||||||
doc.calculate_qty_and_amount(save=True)
|
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)
|
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
|
||||||
@@ -1472,6 +1474,7 @@ def get_valuation_rate(
|
|||||||
currency=None,
|
currency=None,
|
||||||
company=None,
|
company=None,
|
||||||
raise_error_if_no_rate=True,
|
raise_error_if_no_rate=True,
|
||||||
|
batch_no=None,
|
||||||
serial_and_batch_bundle=None,
|
serial_and_batch_bundle=None,
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -1480,6 +1483,25 @@ def get_valuation_rate(
|
|||||||
if not company:
|
if not company:
|
||||||
company = frappe.get_cached_value("Warehouse", warehouse, "company")
|
company = frappe.get_cached_value("Warehouse", warehouse, "company")
|
||||||
|
|
||||||
|
if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
|
||||||
|
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(table)
|
||||||
|
.select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
|
||||||
|
.where(
|
||||||
|
(table.item_code == item_code)
|
||||||
|
& (table.warehouse == warehouse)
|
||||||
|
& (table.batch_no == batch_no)
|
||||||
|
& (table.is_cancelled == 0)
|
||||||
|
& (table.voucher_no != voucher_no)
|
||||||
|
& (table.voucher_type != voucher_type)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
last_valuation_rate = query.run()
|
||||||
|
if last_valuation_rate:
|
||||||
|
return flt(last_valuation_rate[0][0])
|
||||||
|
|
||||||
# Get moving average rate of a specific batch number
|
# Get moving average rate of a specific batch number
|
||||||
if warehouse and serial_and_batch_bundle:
|
if warehouse and serial_and_batch_bundle:
|
||||||
batch_obj = BatchNoValuation(
|
batch_obj = BatchNoValuation(
|
||||||
@@ -1574,8 +1596,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
next_stock_reco_detail = get_next_stock_reco(args)
|
next_stock_reco_detail = get_next_stock_reco(args)
|
||||||
if next_stock_reco_detail:
|
if next_stock_reco_detail:
|
||||||
detail = next_stock_reco_detail[0]
|
detail = next_stock_reco_detail[0]
|
||||||
if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no):
|
|
||||||
regenerate_sle_for_batch_stock_reco(detail)
|
|
||||||
|
|
||||||
# add condition to update SLEs before this date & time
|
# add condition to update SLEs before this date & time
|
||||||
datetime_limit_condition = get_datetime_limit_condition(detail)
|
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||||
@@ -1604,16 +1624,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
|||||||
validate_negative_qty_in_future_sle(args, allow_negative_stock)
|
validate_negative_qty_in_future_sle(args, allow_negative_stock)
|
||||||
|
|
||||||
|
|
||||||
def regenerate_sle_for_batch_stock_reco(detail):
|
|
||||||
doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
|
|
||||||
doc.recalculate_current_qty(detail.item_code, detail.batch_no)
|
|
||||||
|
|
||||||
if not frappe.db.exists(
|
|
||||||
"Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
|
|
||||||
):
|
|
||||||
doc.repost_future_sle_and_gle(force=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_stock_reco_qty_shift(args):
|
def get_stock_reco_qty_shift(args):
|
||||||
stock_reco_qty_shift = 0
|
stock_reco_qty_shift = 0
|
||||||
if args.get("is_cancelled"):
|
if args.get("is_cancelled"):
|
||||||
|
|||||||
@@ -167,13 +167,13 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
)
|
)
|
||||||
self.update_status_updater_args()
|
self.update_status_updater_args()
|
||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.delete_auto_created_batches()
|
|
||||||
self.set_consumed_qty_in_subcontract_order()
|
self.set_consumed_qty_in_subcontract_order()
|
||||||
self.set_subcontracting_order_status()
|
self.set_subcontracting_order_status()
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries_on_cancel()
|
self.make_gl_entries_on_cancel()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
self.update_status()
|
self.update_status()
|
||||||
|
self.delete_auto_created_batches()
|
||||||
|
|
||||||
def validate_items_qty(self):
|
def validate_items_qty(self):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
|
|||||||
@@ -953,6 +953,91 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
|
|
||||||
scr.submit()
|
scr.submit()
|
||||||
|
|
||||||
|
def test_subcontracting_receipt_cancel_with_batch(self):
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
|
||||||
|
# Step - 1: Set Backflush Based On as "BOM"
|
||||||
|
set_backflush_based_on("BOM")
|
||||||
|
|
||||||
|
# Step - 2: Create FG and RM Items
|
||||||
|
fg_item = make_item(
|
||||||
|
properties={"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1}
|
||||||
|
).name
|
||||||
|
rm_item1 = make_item(properties={"is_stock_item": 1}).name
|
||||||
|
rm_item2 = make_item(properties={"is_stock_item": 1}).name
|
||||||
|
make_item("Subcontracted Service Item Test For Batch 1", {"is_stock_item": 0})
|
||||||
|
|
||||||
|
# Step - 3: Create BOM for FG Item
|
||||||
|
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
|
||||||
|
for rm_item in bom.items:
|
||||||
|
self.assertEqual(rm_item.rate, 0)
|
||||||
|
self.assertEqual(rm_item.amount, 0)
|
||||||
|
bom = bom.name
|
||||||
|
|
||||||
|
# Step - 4: Create PO and SCO
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item Test For Batch 1",
|
||||||
|
"qty": 100,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": fg_item,
|
||||||
|
"fg_item_qty": 100,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
for rm_item in sco.supplied_items:
|
||||||
|
self.assertEqual(rm_item.rate, 0)
|
||||||
|
self.assertEqual(rm_item.amount, 0)
|
||||||
|
|
||||||
|
# Step - 5: Inward Raw Materials
|
||||||
|
rm_items = get_rm_items(sco.supplied_items)
|
||||||
|
for rm_item in rm_items:
|
||||||
|
rm_item["rate"] = 100
|
||||||
|
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||||
|
|
||||||
|
# Step - 6: Transfer RM's to Subcontractor
|
||||||
|
se = make_stock_transfer_entry(
|
||||||
|
sco_no=sco.name,
|
||||||
|
rm_items=rm_items,
|
||||||
|
itemwise_details=copy.deepcopy(itemwise_details),
|
||||||
|
)
|
||||||
|
for item in se.items:
|
||||||
|
self.assertEqual(item.qty, 100)
|
||||||
|
self.assertEqual(item.basic_rate, 100)
|
||||||
|
self.assertEqual(item.amount, item.qty * item.basic_rate)
|
||||||
|
|
||||||
|
batch_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Batch",
|
||||||
|
"item": fg_item,
|
||||||
|
"batch_id": frappe.generate_hash(length=10),
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
serial_batch_bundle = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Serial and Batch Bundle",
|
||||||
|
"item_code": fg_item,
|
||||||
|
"warehouse": sco.items[0].warehouse,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"type_of_transaction": "Inward",
|
||||||
|
"voucher_type": "Subcontracting Receipt",
|
||||||
|
"entries": [{"batch_no": batch_doc.name, "qty": 100}],
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
# Step - 7: Create Subcontracting Receipt
|
||||||
|
scr = make_subcontracting_receipt(sco.name)
|
||||||
|
scr.items[0].serial_and_batch_bundle = serial_batch_bundle.name
|
||||||
|
scr.save()
|
||||||
|
scr.submit()
|
||||||
|
scr.load_from_db()
|
||||||
|
|
||||||
|
# Step - 8: Cancel Subcontracting Receipt
|
||||||
|
scr.cancel()
|
||||||
|
self.assertTrue(scr.docstatus == 2)
|
||||||
|
|
||||||
@change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
|
@change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
|
||||||
def test_auto_create_purchase_receipt(self):
|
def test_auto_create_purchase_receipt(self):
|
||||||
fg_item = "Subcontracted Item SA1"
|
fg_item = "Subcontracted Item SA1"
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ frappe.ui.form.on("Issue", {
|
|||||||
|
|
||||||
frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", {
|
frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", {
|
||||||
reason: values.reason,
|
reason: values.reason,
|
||||||
user: frappe.session.user_email
|
user: frappe.session.user_email,
|
||||||
|
doctype: frm.doc.doctype,
|
||||||
|
docname: frm.doc.name,
|
||||||
}, () => {
|
}, () => {
|
||||||
reset_sla.enable_primary_action();
|
reset_sla.enable_primary_action();
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
|
|||||||
@@ -774,10 +774,12 @@ def get_response_and_resolution_duration(doc):
|
|||||||
return priority
|
return priority
|
||||||
|
|
||||||
|
|
||||||
def reset_service_level_agreement(doc, reason, user):
|
@frappe.whitelist()
|
||||||
|
def reset_service_level_agreement(doctype: str, docname: str, reason, user):
|
||||||
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
|
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
|
||||||
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
|
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
|
||||||
|
|
||||||
|
doc = frappe.get_doc(doctype, docname)
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Comment",
|
"doctype": "Comment",
|
||||||
|
|||||||
@@ -26,6 +26,26 @@
|
|||||||
{{ render_homepage_section(homepage.hero_section_doc) }}
|
{{ render_homepage_section(homepage.hero_section_doc) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if homepage.products %}
|
||||||
|
<section class="container section-products my-5">
|
||||||
|
<h3>{{ _('Products') }}</h3>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for item in homepage.products %}
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card h-100 justify-content-between">
|
||||||
|
<img class="card-img-top website-image-extra-large" src="{{ item.image }}" loading="lazy" alt="{{ item.item_name }}"></img>
|
||||||
|
<div class="card-body flex-grow-0">
|
||||||
|
<h5 class="card-title">{{ item.item_name }}</h5>
|
||||||
|
<a href="{{ item.route }}" class="card-link">{{ _('More details') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if blogs %}
|
{% if blogs %}
|
||||||
<section class="container my-5">
|
<section class="container my-5">
|
||||||
<h3>{{ _('Publications') }}</h3>
|
<h3>{{ _('Publications') }}</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user