mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-26 01:58:31 +00:00
Merge pull request #45005 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -128,7 +128,7 @@
|
||||
"description": "Rate at which this tax is applied",
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"oldfieldname": "tax_rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"oldfieldname": "rate",
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
|
||||
get_amounts_not_reflected_in_system,
|
||||
get_entries,
|
||||
@@ -304,54 +305,56 @@ def create_payment_entry_bts(
|
||||
bank_transaction = frappe.db.get_values(
|
||||
"Bank Transaction",
|
||||
bank_transaction_name,
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
|
||||
fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"],
|
||||
as_dict=True,
|
||||
)[0]
|
||||
paid_amount = bank_transaction.unallocated_amount
|
||||
|
||||
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
|
||||
|
||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_value("Account", company_account, "company")
|
||||
payment_entry_dict = {
|
||||
"company": company,
|
||||
"payment_type": payment_type,
|
||||
"reference_no": reference_number,
|
||||
"reference_date": reference_date,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"posting_date": posting_date,
|
||||
"paid_amount": paid_amount,
|
||||
"received_amount": paid_amount,
|
||||
}
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
|
||||
company = frappe.get_cached_value("Account", bank_account, "company")
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
|
||||
payment_entry.update(payment_entry_dict)
|
||||
bank_currency = bank_transaction.currency
|
||||
party_currency = frappe.get_cached_value("Account", party_account, "account_currency")
|
||||
|
||||
if mode_of_payment:
|
||||
payment_entry.mode_of_payment = mode_of_payment
|
||||
if project:
|
||||
payment_entry.project = project
|
||||
if cost_center:
|
||||
payment_entry.cost_center = cost_center
|
||||
if payment_type == "Receive":
|
||||
payment_entry.paid_to = company_account
|
||||
else:
|
||||
payment_entry.paid_from = company_account
|
||||
exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date)
|
||||
|
||||
payment_entry.validate()
|
||||
amt_in_bank_acc_currency = bank_transaction.unallocated_amount
|
||||
amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = payment_type
|
||||
pe.company = company
|
||||
pe.reference_no = reference_number
|
||||
pe.reference_date = reference_date
|
||||
pe.party_type = party_type
|
||||
pe.party = party
|
||||
pe.posting_date = posting_date
|
||||
pe.paid_from = party_account if payment_type == "Receive" else bank_account
|
||||
pe.paid_to = party_account if payment_type == "Pay" else bank_account
|
||||
pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency
|
||||
pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency
|
||||
pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency
|
||||
pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency
|
||||
pe.mode_of_payment = mode_of_payment
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
return payment_entry
|
||||
return pe
|
||||
|
||||
payment_entry.insert()
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
payment_entry.submit()
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment_entry.name,
|
||||
"amount": paid_amount,
|
||||
"payment_name": pe.name,
|
||||
"amount": amt_in_bank_acc_currency,
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -480,8 +483,12 @@ def get_linked_payments(
|
||||
def subtract_allocations(gl_account, vouchers):
|
||||
"Look up & subtract any existing Bank Transaction allocations"
|
||||
copied = []
|
||||
|
||||
voucher_docs = [(voucher.get("doctype"), voucher.get("name")) for voucher in vouchers]
|
||||
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
|
||||
|
||||
for voucher in vouchers:
|
||||
rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
|
||||
rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or []
|
||||
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
|
||||
|
||||
if amount := None if not filtered_row else filtered_row[0]["total"]:
|
||||
@@ -719,7 +726,7 @@ def get_pe_matching_query(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.base_paid_amount_after_tax.as_("paid_amount"),
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
|
||||
@@ -154,10 +154,16 @@ class BankTransaction(Document):
|
||||
"""
|
||||
remaining_amount = self.unallocated_amount
|
||||
to_remove = []
|
||||
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
|
||||
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
self, payment_entry
|
||||
self,
|
||||
payment_entry,
|
||||
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
|
||||
or [],
|
||||
)
|
||||
|
||||
if 0.0 == unallocated_amount:
|
||||
@@ -232,7 +238,7 @@ def get_doctypes_for_bank_reconciliation():
|
||||
return frappe.get_hooks("bank_reconciliation_doctypes")
|
||||
|
||||
|
||||
def get_clearance_details(transaction, payment_entry):
|
||||
def get_clearance_details(transaction, payment_entry, bt_allocations):
|
||||
"""
|
||||
There should only be one bank gle for a voucher.
|
||||
Could be none for a Bank Transaction.
|
||||
@@ -241,7 +247,6 @@ def get_clearance_details(transaction, payment_entry):
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
@@ -294,44 +299,52 @@ def get_related_bank_gl_entries(doctype, docname):
|
||||
)
|
||||
|
||||
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
def get_total_allocated_amount(docs):
|
||||
"""
|
||||
Gets the sum of allocations for a voucher on each bank GL account
|
||||
along with the latest bank transaction name & date
|
||||
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
|
||||
"""
|
||||
if not docs:
|
||||
return {}
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
SELECT total, latest_name, latest_date, gl_account FROM (
|
||||
SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
|
||||
SELECT
|
||||
ROW_NUMBER() OVER w AS rownum,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
|
||||
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
|
||||
FIRST_VALUE(bt.name) OVER w AS latest_name,
|
||||
FIRST_VALUE(bt.date) OVER w AS latest_date,
|
||||
ba.account AS gl_account
|
||||
ba.account AS gl_account,
|
||||
btp.payment_document,
|
||||
btp.payment_entry
|
||||
FROM
|
||||
`tabBank Transaction Payments` btp
|
||||
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
|
||||
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
|
||||
WHERE
|
||||
btp.payment_document = %(doctype)s
|
||||
AND btp.payment_entry = %(docname)s
|
||||
(btp.payment_document, btp.payment_entry) IN %(docs)s
|
||||
AND bt.docstatus = 1
|
||||
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
|
||||
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
|
||||
) temp
|
||||
WHERE
|
||||
rownum = 1
|
||||
""",
|
||||
dict(doctype=doctype, docname=docname),
|
||||
dict(docs=docs),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
payment_allocation_details = {}
|
||||
for row in result:
|
||||
# Why is this *sometimes* a byte string?
|
||||
if isinstance(row["latest_name"], bytes):
|
||||
row["latest_name"] = row["latest_name"].decode()
|
||||
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
|
||||
return result
|
||||
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
|
||||
|
||||
return payment_allocation_details
|
||||
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
|
||||
@@ -13,7 +13,11 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
|
||||
from erpnext.accounts.party import (
|
||||
validate_account_party_type,
|
||||
validate_party_frozen_disabled,
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
@@ -268,6 +272,7 @@ class GLEntry(Document):
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
|
||||
@@ -79,3 +79,48 @@ class TestGLEntry(unittest.TestCase):
|
||||
"SELECT current from tabSeries where name = %s", naming_series
|
||||
)[0][0]
|
||||
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
|
||||
|
||||
def test_validate_account_party_type(self):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Supplier"
|
||||
break
|
||||
|
||||
jv.save()
|
||||
try:
|
||||
jv.submit()
|
||||
except Exception as e:
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
jv1 = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
100,
|
||||
"_Test Cost Center - _TC",
|
||||
save=False,
|
||||
submit=False,
|
||||
)
|
||||
|
||||
for row in jv.accounts:
|
||||
row.party_type = "Customer"
|
||||
break
|
||||
|
||||
jv1.save()
|
||||
try:
|
||||
jv1.submit()
|
||||
except Exception as e:
|
||||
self.assertEqual(
|
||||
str(e),
|
||||
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
|
||||
)
|
||||
|
||||
@@ -27,6 +27,18 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
// project excluded in setup_dimension_filters
|
||||
frm.set_query("project", function (doc) {
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (doc.party_type == "Customer") filters.customer = doc.party;
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
}
|
||||
|
||||
@@ -1479,7 +1479,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Paid",
|
||||
company=company,
|
||||
account_type="Liability",
|
||||
account_type="Payable",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Grand Total') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Grand Total") }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Net Total') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Net Total") }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left font-bold">{{ _('Total Quantity') }}</td>
|
||||
<td class="text-left font-bold">{{ _("Total Quantity") }}</td>
|
||||
<td class='text-right'>{{ data.total_quantity or '' }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<tbody>
|
||||
{% for d in data.payment_reconciliation %}
|
||||
<tr>
|
||||
<td class="text-left">{{ d.mode_of_payment }}</td>
|
||||
<td class="text-left">{{ _(d.mode_of_payment) }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -63,7 +63,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ _("Account") }}</th>
|
||||
<th class="text-left">{{ _("Rate") }}</th>
|
||||
<th class="text-left">{{ _("Tax Rate") }}</th>
|
||||
<th class="text-right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"label": "Tax Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -399,6 +399,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
hide_fields(this.frm.doc);
|
||||
if (cint(this.frm.doc.is_paid)) {
|
||||
this.frm.set_value("allocate_advances_automatically", 0);
|
||||
this.frm.set_value("payment_terms_template", "");
|
||||
this.frm.set_value("payment_schedule", []);
|
||||
if (!this.frm.doc.company) {
|
||||
this.frm.set_value("is_paid", 0);
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
|
||||
@@ -507,7 +507,7 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
def validate_pos_paid_amount(self):
|
||||
if len(self.payments) == 0 and self.is_pos:
|
||||
if len(self.payments) == 0 and self.is_pos and flt(self.grand_total) > 0:
|
||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||
|
||||
def check_if_consolidated_invoice(self):
|
||||
|
||||
@@ -43,6 +43,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.get_item_details import get_item_tax_map
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
@@ -2873,13 +2874,26 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
item.save()
|
||||
|
||||
sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True)
|
||||
item_tax_map = get_item_tax_map(
|
||||
company=sales_invoice.company,
|
||||
item_tax_template=sales_invoice.items[0].item_tax_template,
|
||||
)
|
||||
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
# Apply discount
|
||||
sales_invoice.apply_discount_on = "Net Total"
|
||||
sales_invoice.discount_amount = 300
|
||||
sales_invoice.save()
|
||||
|
||||
item_tax_map = get_item_tax_map(
|
||||
company=sales_invoice.company,
|
||||
item_tax_template=sales_invoice.items[0].item_tax_template,
|
||||
)
|
||||
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
||||
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
|
||||
|
||||
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
|
||||
def test_sales_invoice_with_discount_accounting_enabled(self):
|
||||
|
||||
@@ -759,6 +759,17 @@ def validate_party_frozen_disabled(party_type, party_name):
|
||||
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
|
||||
|
||||
|
||||
def validate_account_party_type(self):
|
||||
if self.party_type and self.party:
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type and (account_type not in ["Receivable", "Payable"]):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"
|
||||
).format(self.account)
|
||||
)
|
||||
|
||||
|
||||
def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True)
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Rate"),
|
||||
"label": _("Tax Rate"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Float",
|
||||
"options": "currency",
|
||||
|
||||
@@ -130,7 +130,9 @@ def get_fiscal_years(
|
||||
else:
|
||||
return ((fy.name, fy.year_start_date, fy.year_end_date),)
|
||||
|
||||
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
|
||||
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(
|
||||
_(label), formatdate(transaction_date)
|
||||
)
|
||||
if company:
|
||||
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from frappe.utils import (
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_comma_separated_links,
|
||||
@@ -887,6 +888,7 @@ def get_asset_naming_series():
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(asset, item_code, company, serial_no=None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
si.company = company
|
||||
si.currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
@@ -903,6 +905,16 @@ def make_sales_invoice(asset, item_code, company, serial_no=None):
|
||||
"qty": 1,
|
||||
},
|
||||
)
|
||||
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
si.update(
|
||||
{
|
||||
dimension["fieldname"]: asset_doc.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
si.set_missing_values()
|
||||
return si
|
||||
|
||||
|
||||
@@ -18,11 +18,12 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
update_received_amount(data)
|
||||
|
||||
if not data:
|
||||
return [], [], None, []
|
||||
|
||||
update_received_amount(data)
|
||||
|
||||
data, chart_data = prepare_data(data, filters)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
@@ -103,6 +104,11 @@ def get_received_amount_data(data):
|
||||
pr = frappe.qb.DocType("Purchase Receipt")
|
||||
pr_item = frappe.qb.DocType("Purchase Receipt Item")
|
||||
|
||||
po_items = [row.name for row in data]
|
||||
|
||||
if not po_items:
|
||||
return frappe._dict()
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pr)
|
||||
.inner_join(pr_item)
|
||||
@@ -111,12 +117,10 @@ def get_received_amount_data(data):
|
||||
pr_item.purchase_order_item,
|
||||
Sum(pr_item.base_amount).as_("received_qty_amount"),
|
||||
)
|
||||
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
|
||||
.where((pr.docstatus == 1) & (pr_item.purchase_order_item.isin(po_items)))
|
||||
.groupby(pr_item.purchase_order_item)
|
||||
)
|
||||
|
||||
query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
|
||||
|
||||
data = query.run()
|
||||
|
||||
if not data:
|
||||
|
||||
@@ -464,9 +464,16 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_invoice_documents_schedule(self):
|
||||
if self.is_return:
|
||||
if (
|
||||
self.is_return
|
||||
or (self.doctype == "Purchase Invoice" and self.is_paid)
|
||||
or (self.doctype == "Sales Invoice" and self.is_pos)
|
||||
or self.get("is_opening") == "Yes"
|
||||
):
|
||||
self.payment_terms_template = ""
|
||||
self.payment_schedule = []
|
||||
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
self.validate_payment_schedule_dates()
|
||||
@@ -1219,7 +1226,7 @@ class AccountsController(TransactionBase):
|
||||
party_account = []
|
||||
default_advance_account = None
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
if self.doctype in ["Sales Invoice", "POS Invoice"]:
|
||||
party_type = "Customer"
|
||||
party = self.customer
|
||||
amount_field = "credit_in_account_currency"
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
@@ -744,6 +745,7 @@ class BuyingController(SubcontractingController):
|
||||
def auto_make_assets(self, asset_items):
|
||||
items_data = get_asset_item_details(asset_items)
|
||||
messages = []
|
||||
accounting_dimensions = get_dimensions(with_cost_center_and_project=True)
|
||||
|
||||
for d in self.items:
|
||||
if d.is_fixed_asset:
|
||||
@@ -755,11 +757,11 @@ class BuyingController(SubcontractingController):
|
||||
if item_data.get("asset_naming_series"):
|
||||
created_assets = []
|
||||
if item_data.get("is_grouped_asset"):
|
||||
asset = self.make_asset(d, is_grouped_asset=True)
|
||||
asset = self.make_asset(d, accounting_dimensions, is_grouped_asset=True)
|
||||
created_assets.append(asset)
|
||||
else:
|
||||
for _qty in range(cint(d.qty)):
|
||||
asset = self.make_asset(d)
|
||||
asset = self.make_asset(d, accounting_dimensions)
|
||||
created_assets.append(asset)
|
||||
|
||||
if len(created_assets) > 5:
|
||||
@@ -797,7 +799,7 @@ class BuyingController(SubcontractingController):
|
||||
for message in messages:
|
||||
frappe.msgprint(message, title="Success", indicator="green")
|
||||
|
||||
def make_asset(self, row, is_grouped_asset=False):
|
||||
def make_asset(self, row, accounting_dimensions, is_grouped_asset=False):
|
||||
if not row.asset_location:
|
||||
frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code))
|
||||
|
||||
@@ -828,6 +830,13 @@ class BuyingController(SubcontractingController):
|
||||
"purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
)
|
||||
for dimension in accounting_dimensions[0]:
|
||||
asset.update(
|
||||
{
|
||||
dimension["fieldname"]: self.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
asset.flags.ignore_validate = True
|
||||
asset.flags.ignore_mandatory = True
|
||||
|
||||
@@ -271,10 +271,14 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
qb_filter_or_conditions = []
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
if filters and filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
)
|
||||
if filters:
|
||||
if filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
qb_filter_and_conditions.append(proj.company == filters.get("company"))
|
||||
|
||||
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"]))
|
||||
|
||||
|
||||
@@ -126,9 +126,13 @@ status_map = {
|
||||
"Partially Received",
|
||||
"eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'",
|
||||
],
|
||||
[
|
||||
"Partially Received",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'",
|
||||
],
|
||||
[
|
||||
"Partially Ordered",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'",
|
||||
],
|
||||
[
|
||||
"Manufactured",
|
||||
|
||||
@@ -18,7 +18,7 @@ from erpnext.controllers.accounts_controller import (
|
||||
validate_inclusive_tax,
|
||||
validate_taxes_and_charges,
|
||||
)
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template, get_item_tax_map
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ class calculate_taxes_and_totals:
|
||||
self.validate_conversion_rate()
|
||||
self.calculate_item_values()
|
||||
self.validate_item_tax_template()
|
||||
self.update_item_tax_map()
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
self.calculate_net_total()
|
||||
@@ -134,6 +135,14 @@ class calculate_taxes_and_totals:
|
||||
)
|
||||
)
|
||||
|
||||
def update_item_tax_map(self):
|
||||
for item in self.doc.items:
|
||||
item.item_tax_rate = get_item_tax_map(
|
||||
company=self.doc.get("company"),
|
||||
item_tax_template=item.item_tax_template,
|
||||
as_json=True,
|
||||
)
|
||||
|
||||
def validate_conversion_rate(self):
|
||||
# validate conversion rate
|
||||
company_currency = erpnext.get_company_currency(self.doc.company)
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"column_break_8",
|
||||
"order_no",
|
||||
"order_date",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"company",
|
||||
@@ -129,15 +131,27 @@
|
||||
"fieldname": "terms",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Terms and Conditions Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "order_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Order No"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.order_no",
|
||||
"fieldname": "order_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Order Date"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-29 00:30:30.621636",
|
||||
"modified": "2024-12-05 15:44:21.520093",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Blanket Order",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -31,6 +31,8 @@ class BlanketOrder(Document):
|
||||
from_date: DF.Date
|
||||
items: DF.Table[BlanketOrderItem]
|
||||
naming_series: DF.Literal["MFG-BLR-.YYYY.-"]
|
||||
order_date: DF.Date | None
|
||||
order_no: DF.Data | None
|
||||
supplier: DF.Link | None
|
||||
supplier_name: DF.Data | None
|
||||
tc_name: DF.Link | None
|
||||
|
||||
@@ -2,13 +2,13 @@ frappe.listview_settings["BOM"] = {
|
||||
add_fields: ["is_active", "is_default", "total_cost", "has_variants"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.is_active && doc.has_variants) {
|
||||
return [__("Template"), "orange", "has_variants,=,Yes"];
|
||||
return [__("Template"), "orange", "has_variants,=,1"];
|
||||
} else if (doc.is_default) {
|
||||
return [__("Default"), "green", "is_default,=,Yes"];
|
||||
return [__("Default"), "green", "is_default,=,1"];
|
||||
} else if (doc.is_active) {
|
||||
return [__("Active"), "blue", "is_active,=,Yes"];
|
||||
return [__("Active"), "blue", "is_active,=,1"];
|
||||
} else if (!doc.is_active) {
|
||||
return [__("Not active"), "gray", "is_active,=,No"];
|
||||
return [__("Not active"), "gray", "is_active,=,0"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -230,8 +230,8 @@ class ForecastingReport(ExponentialSmoothingForecast):
|
||||
"data": {
|
||||
"labels": labels,
|
||||
"datasets": [
|
||||
{"name": "Demand", "values": self.total_demand},
|
||||
{"name": "Forecast", "values": self.total_forecast},
|
||||
{"name": _("Demand"), "values": self.total_demand},
|
||||
{"name": _("Forecast"), "values": self.total_forecast},
|
||||
],
|
||||
},
|
||||
"type": "line",
|
||||
|
||||
@@ -106,7 +106,7 @@ def get_data(filters, columns):
|
||||
|
||||
for label in labels:
|
||||
work = {}
|
||||
work["Status"] = label
|
||||
work["Status"] = _(label)
|
||||
for _dummy, end_date in ranges:
|
||||
period = get_period(end_date, filters)
|
||||
if periodic_data.get(label).get(period):
|
||||
|
||||
@@ -4,61 +4,67 @@ import frappe
|
||||
|
||||
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
|
||||
|
||||
DEFAULT_FILTERS = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2010-01-01",
|
||||
"to_date": "2030-01-01",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
|
||||
|
||||
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
|
||||
("BOM Operations Time", {}),
|
||||
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
|
||||
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
|
||||
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
|
||||
("Downtime Analysis", {}),
|
||||
(
|
||||
"Exponential Smoothing Forecasting",
|
||||
{
|
||||
"based_on_document": "Sales Order",
|
||||
"based_on_field": "Qty",
|
||||
"no_of_years": 3,
|
||||
"periodicity": "Yearly",
|
||||
"smoothing_constant": 0.3,
|
||||
},
|
||||
),
|
||||
("Job Card Summary", {"fiscal_year": "2021-2022"}),
|
||||
("Production Analytics", {"range": "Monthly"}),
|
||||
("Quality Inspection Summary", {}),
|
||||
("Process Loss Report", {}),
|
||||
("Work Order Stock Report", {}),
|
||||
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
|
||||
]
|
||||
|
||||
|
||||
if frappe.db.a_row_exists("Production Plan"):
|
||||
REPORT_FILTER_TEST_CASES.append(
|
||||
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
|
||||
)
|
||||
|
||||
OPTIONAL_FILTERS = {
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item": "_Test Item",
|
||||
"item_group": "_Test Item Group",
|
||||
}
|
||||
|
||||
|
||||
class TestManufacturingReports(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.setup_default_filters()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def setup_default_filters(self):
|
||||
self.last_bom = frappe.get_last_doc("BOM").name
|
||||
self.DEFAULT_FILTERS = {
|
||||
"company": "_Test Company",
|
||||
"from_date": "2010-01-01",
|
||||
"to_date": "2030-01-01",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
|
||||
self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("BOM Explorer", {"bom": self.last_bom}),
|
||||
("BOM Operations Time", {}),
|
||||
("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}),
|
||||
("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}),
|
||||
("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
|
||||
("Downtime Analysis", {}),
|
||||
(
|
||||
"Exponential Smoothing Forecasting",
|
||||
{
|
||||
"based_on_document": "Sales Order",
|
||||
"based_on_field": "Qty",
|
||||
"no_of_years": 3,
|
||||
"periodicity": "Yearly",
|
||||
"smoothing_constant": 0.3,
|
||||
},
|
||||
),
|
||||
("Job Card Summary", {"fiscal_year": "2021-2022"}),
|
||||
("Production Analytics", {"range": "Monthly"}),
|
||||
("Quality Inspection Summary", {}),
|
||||
("Process Loss Report", {}),
|
||||
("Work Order Stock Report", {}),
|
||||
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
|
||||
]
|
||||
|
||||
if frappe.db.a_row_exists("Production Plan"):
|
||||
self.REPORT_FILTER_TEST_CASES.append(
|
||||
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
|
||||
)
|
||||
|
||||
self.OPTIONAL_FILTERS = {
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item": "_Test Item",
|
||||
"item_group": "_Test Item Group",
|
||||
}
|
||||
|
||||
def test_execute_all_manufacturing_reports(self):
|
||||
"""Test that all script report in manufacturing modules are executable with supported filters"""
|
||||
for report, filter in REPORT_FILTER_TEST_CASES:
|
||||
for report, filter in self.REPORT_FILTER_TEST_CASES:
|
||||
with self.subTest(report=report):
|
||||
execute_script_report(
|
||||
report_name=report,
|
||||
module="Manufacturing",
|
||||
filters=filter,
|
||||
default_filters=DEFAULT_FILTERS,
|
||||
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||
default_filters=self.DEFAULT_FILTERS,
|
||||
optional_filters=self.OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||
)
|
||||
|
||||
@@ -147,10 +147,10 @@ class BOMConfigurator {
|
||||
if (!node.expanded) {
|
||||
view.tree.load_children(node, true);
|
||||
$(node.parent[0]).find(".tree-children").show();
|
||||
node.$toolbar.find(".expand-all-btn").html("Collapse All");
|
||||
node.$toolbar.find(".expand-all-btn").html(__("Collapse All"));
|
||||
} else {
|
||||
node.$tree_link.trigger("click");
|
||||
node.$toolbar.find(".expand-all-btn").html("Expand All");
|
||||
node.$toolbar.find(".expand-all-btn").html(__("Expand All"));
|
||||
}
|
||||
},
|
||||
condition: function (node) {
|
||||
@@ -190,10 +190,10 @@ class BOMConfigurator {
|
||||
if (!node.expanded) {
|
||||
view.tree.load_children(node, true);
|
||||
$(node.parent[0]).find(".tree-children").show();
|
||||
node.$toolbar.find(".expand-all-btn").html("Collapse All");
|
||||
node.$toolbar.find(".expand-all-btn").html(__("Collapse All"));
|
||||
} else {
|
||||
node.$tree_link.trigger("click");
|
||||
node.$toolbar.find(".expand-all-btn").html("Expand All");
|
||||
node.$toolbar.find(".expand-all-btn").html(__("Expand All"));
|
||||
}
|
||||
},
|
||||
condition: function (node) {
|
||||
|
||||
@@ -25,6 +25,14 @@ erpnext.buying = {
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("project", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (this.frm.doc.__islocal
|
||||
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
|
||||
|
||||
@@ -145,6 +153,18 @@ erpnext.buying = {
|
||||
});
|
||||
}
|
||||
|
||||
company(){
|
||||
if(!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return;
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||
args: { name: this.frm.doc.company, existing_address:this.frm.doc.billing_address },
|
||||
callback: (r) => {
|
||||
this.frm.set_value("billing_address", r.message || "");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
supplier_address() {
|
||||
erpnext.utils.get_address_display(this.frm);
|
||||
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");
|
||||
|
||||
@@ -813,6 +813,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
validate() {
|
||||
this.apply_pricing_rule()
|
||||
this.calculate_taxes_and_totals(false);
|
||||
}
|
||||
|
||||
@@ -959,7 +960,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
|
||||
|
||||
if (!is_drop_ship) {
|
||||
console.log('get_shipping_address');
|
||||
erpnext.utils.get_shipping_address(this.frm, function() {
|
||||
set_party_account(set_pricing);
|
||||
});
|
||||
@@ -975,6 +975,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
transaction_date() {
|
||||
this.apply_pricing_rule()
|
||||
if (this.frm.doc.transaction_date) {
|
||||
this.frm.transaction_date = this.frm.doc.transaction_date;
|
||||
frappe.ui.form.trigger(this.frm.doc.doctype, "currency");
|
||||
@@ -983,6 +984,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
posting_date() {
|
||||
var me = this;
|
||||
me.apply_pricing_rule()
|
||||
if (this.frm.doc.posting_date) {
|
||||
this.frm.posting_date = this.frm.doc.posting_date;
|
||||
|
||||
@@ -2310,6 +2312,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
fieldname: "batch_no",
|
||||
label: __("Batch No"),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "child_row_reference",
|
||||
label: __("Child Row Reference"),
|
||||
hidden: true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2353,14 +2361,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
if (this.has_inspection_required(item)) {
|
||||
let dialog_items = dialog.fields_dict.items;
|
||||
dialog_items.df.data.push({
|
||||
"docname": item.name,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"description": item.description,
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no,
|
||||
"sample_size": item.sample_quantity
|
||||
"sample_size": item.sample_quantity,
|
||||
"child_row_reference": item.name,
|
||||
});
|
||||
dialog_items.grid.refresh();
|
||||
}
|
||||
|
||||
@@ -285,6 +285,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
edit_cart: () => this.payment.edit_cart(),
|
||||
|
||||
customer_details_updated: (details) => {
|
||||
this.item_selector.load_items_data();
|
||||
this.customer_details = details;
|
||||
// will add/remove LP payment method
|
||||
this.payment.render_loyalty_points_payment_mode();
|
||||
|
||||
@@ -390,6 +390,14 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
input_class: "input-xs",
|
||||
onchange: function () {
|
||||
this.value = flt(this.value);
|
||||
if (this.value > 100) {
|
||||
frappe.msgprint({
|
||||
title: __("Invalid Discount"),
|
||||
indicator: "red",
|
||||
message: __("Discount cannot be greater than 100%."),
|
||||
});
|
||||
this.value = 0;
|
||||
}
|
||||
frappe.model.set_value(
|
||||
frm.doc.doctype,
|
||||
frm.doc.name,
|
||||
|
||||
@@ -315,8 +315,12 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
|
||||
const field_control = this[`${fieldname}_control`];
|
||||
const item_row_is_being_edited = this.compare_with_current_item(item_row);
|
||||
|
||||
if (item_row_is_being_edited && field_control && field_control.get_value() !== value) {
|
||||
if (
|
||||
item_row_is_being_edited &&
|
||||
field_control &&
|
||||
field_control.get_value() !== value &&
|
||||
value == item_row[fieldname]
|
||||
) {
|
||||
field_control.set_value(value);
|
||||
cur_pos.update_cart_html(item_row);
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
|
||||
// for setting correct amount after loyalty points are redeemed
|
||||
const default_mop = locals[cdt][cdn];
|
||||
const mode = default_mop.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||
const mode = this.sanitize_mode_of_payment(default_mop.mode_of_payment);
|
||||
if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) {
|
||||
this[`${mode}_control`].set_value(default_mop.amount);
|
||||
}
|
||||
@@ -388,7 +388,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
this.$payment_modes.html(
|
||||
`${payments
|
||||
.map((p, i) => {
|
||||
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
|
||||
const payment_type = p.type;
|
||||
const margin = i % 2 === 0 ? "pr-2" : "pl-2";
|
||||
const amount = p.amount > 0 ? format_currency(p.amount, currency) : "";
|
||||
@@ -407,7 +407,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
);
|
||||
|
||||
payments.forEach((p) => {
|
||||
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
|
||||
const me = this;
|
||||
this[`${mode}_control`] = frappe.ui.form.make_control({
|
||||
df: {
|
||||
@@ -442,7 +442,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
const doc = this.events.get_frm().doc;
|
||||
const payments = doc.payments;
|
||||
payments.forEach((p) => {
|
||||
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
|
||||
const mode = this.sanitize_mode_of_payment(p.mode_of_payment);
|
||||
if (p.default) {
|
||||
setTimeout(() => {
|
||||
this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
|
||||
@@ -612,4 +612,12 @@ erpnext.PointOfSale.Payment = class {
|
||||
toggle_component(show) {
|
||||
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
|
||||
}
|
||||
|
||||
sanitize_mode_of_payment(mode_of_payment) {
|
||||
return mode_of_payment
|
||||
.replace(/ +/g, "_")
|
||||
.replace(/[^\p{L}\p{N}_-]/gu, "")
|
||||
.replace(/^[^_a-zA-Z\p{L}]+/u, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -292,7 +292,7 @@ def get_or_create_tax_group(company_name, root_type):
|
||||
|
||||
tax_group_account.flags.ignore_links = True
|
||||
tax_group_account.flags.ignore_validate = True
|
||||
tax_group_account.insert(ignore_permissions=True)
|
||||
tax_group_account.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
tax_group_name = tax_group_account.name
|
||||
|
||||
|
||||
@@ -181,6 +181,9 @@ class DeprecatedBatchNoValuation:
|
||||
stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
|
||||
self.stock_value_change += stock_value_change
|
||||
|
||||
self.non_batchwise_balance_value[batch_no] -= stock_value_change
|
||||
self.non_batchwise_balance_qty[batch_no] -= ledger.qty
|
||||
|
||||
frappe.db.set_value(
|
||||
"Serial and Batch Entry",
|
||||
ledger.name,
|
||||
@@ -220,7 +223,6 @@ class DeprecatedBatchNoValuation:
|
||||
.select(
|
||||
sle.batch_no,
|
||||
Sum(sle.actual_qty).as_("batch_qty"),
|
||||
Sum(sle.stock_value_difference).as_("batch_value"),
|
||||
)
|
||||
.where(
|
||||
(sle.item_code == self.sle.item_code)
|
||||
@@ -237,11 +239,59 @@ class DeprecatedBatchNoValuation:
|
||||
if self.sle.name:
|
||||
query = query.where(sle.name != self.sle.name)
|
||||
|
||||
for d in query.run(as_dict=True):
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
|
||||
batch_data = query.run(as_dict=True)
|
||||
for d in batch_data:
|
||||
self.available_qty[d.batch_no] += flt(d.batch_qty)
|
||||
|
||||
last_sle = self.get_last_sle_for_non_batch()
|
||||
for d in batch_data:
|
||||
self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value)
|
||||
self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction)
|
||||
|
||||
def get_last_sle_for_non_batch(self):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
|
||||
posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time)
|
||||
if not self.sle.creation:
|
||||
posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1)
|
||||
|
||||
timestamp_condition = sle.posting_datetime < posting_datetime
|
||||
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (sle.posting_datetime == posting_datetime) & (
|
||||
sle.creation < self.sle.creation
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.inner_join(batch)
|
||||
.on(sle.batch_no == batch.name)
|
||||
.select(
|
||||
sle.stock_value,
|
||||
sle.qty_after_transaction,
|
||||
)
|
||||
.where(
|
||||
(sle.item_code == self.sle.item_code)
|
||||
& (sle.warehouse == self.sle.warehouse)
|
||||
& (sle.batch_no.isnotnull())
|
||||
& (batch.use_batchwise_valuation == 0)
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.where(timestamp_condition)
|
||||
.orderby(sle.posting_datetime, order=Order.desc)
|
||||
.orderby(sle.creation, order=Order.desc)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
if self.sle.name:
|
||||
query = query.where(sle.name != self.sle.name)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
return data[0] if data else {}
|
||||
|
||||
@deprecated
|
||||
def set_balance_value_from_bundle(self) -> None:
|
||||
bundle = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
|
||||
@@ -65,7 +65,7 @@ class ClosingStockBalance(Document):
|
||||
& (
|
||||
(table.from_date.between(self.from_date, self.to_date))
|
||||
| (table.to_date.between(self.from_date, self.to_date))
|
||||
| ((table.from_date >= self.from_date) & (table.to_date >= self.to_date))
|
||||
| ((self.from_date >= table.from_date) & (table.from_date >= self.to_date))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -237,8 +237,13 @@ class InventoryDimension(Document):
|
||||
custom_fields["Stock Ledger Entry"] = dimension_field
|
||||
|
||||
filter_custom_fields = {}
|
||||
ignore_doctypes = ["Serial and Batch Bundle", "Serial and Batch Entry", "Pick List Item"]
|
||||
|
||||
if custom_fields:
|
||||
for doctype, fields in custom_fields.items():
|
||||
if doctype in ignore_doctypes:
|
||||
continue
|
||||
|
||||
if isinstance(fields, dict):
|
||||
fields = [fields]
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ frappe.ui.form.on("Material Request", {
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
primary_action_label: "Get Items",
|
||||
primary_action_label: __("Get Items"),
|
||||
primary_action(values) {
|
||||
if (!values) return;
|
||||
values["company"] = frm.doc.company;
|
||||
|
||||
@@ -766,6 +766,7 @@ def raise_work_orders(material_request):
|
||||
)
|
||||
|
||||
wo_order.set_work_order_operations()
|
||||
wo_order.flags.ignore_validate = True
|
||||
wo_order.flags.ignore_mandatory = True
|
||||
wo_order.save()
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@ frappe.listview_settings["Material Request"] = {
|
||||
}
|
||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
|
||||
return [__("Pending"), "orange", "per_ordered,=,0"];
|
||||
} else if (
|
||||
doc.docstatus == 1 &&
|
||||
flt(doc.per_ordered, precision) < 100 &&
|
||||
doc.material_request_type == "Material Transfer"
|
||||
) {
|
||||
return [__("Partially Received"), "yellow", "per_ordered,<,100"];
|
||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) {
|
||||
return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
|
||||
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) {
|
||||
|
||||
@@ -17,6 +17,7 @@ from erpnext.stock.doctype.material_request.material_request import (
|
||||
make_supplier_quotation,
|
||||
raise_work_orders,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
|
||||
class TestMaterialRequest(FrappeTestCase):
|
||||
@@ -59,6 +60,43 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
self.assertEqual(se.doctype, "Stock Entry")
|
||||
self.assertEqual(len(se.get("items")), len(mr.get("items")))
|
||||
|
||||
def test_partial_make_stock_entry(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry as _make_stock_entry
|
||||
|
||||
mr = frappe.copy_doc(test_records[0]).insert()
|
||||
|
||||
source_wh = create_warehouse(
|
||||
warehouse_name="_Test Source Warehouse",
|
||||
properties={"parent_warehouse": "All Warehouses - _TC"},
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
mr = frappe.get_doc("Material Request", mr.name)
|
||||
mr.material_request_type = "Material Transfer"
|
||||
|
||||
for row in mr.items:
|
||||
_make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
qty=10,
|
||||
to_warehouse=source_wh,
|
||||
company="_Test Company",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
row.from_warehouse = source_wh
|
||||
row.qty = 10
|
||||
|
||||
mr.save()
|
||||
mr.submit()
|
||||
|
||||
se = make_stock_entry(mr.name)
|
||||
se.get("items")[0].qty = 5
|
||||
se.insert()
|
||||
se.submit()
|
||||
|
||||
mr.reload()
|
||||
self.assertEqual(mr.status, "Partially Received")
|
||||
|
||||
def test_in_transit_make_stock_entry(self):
|
||||
mr = frappe.copy_doc(test_records[0]).insert()
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"inspection_type",
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"child_row_reference",
|
||||
"section_break_7",
|
||||
"item_code",
|
||||
"item_serial_no",
|
||||
@@ -238,6 +239,15 @@
|
||||
"fieldname": "manual_inspection",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manual Inspection"
|
||||
},
|
||||
{
|
||||
"fieldname": "child_row_reference",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Child Row Reference",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-search",
|
||||
@@ -245,7 +255,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-23 11:56:50.282878",
|
||||
"modified": "2024-12-30 19:08:16.611192",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Quality Inspection",
|
||||
@@ -272,4 +282,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class QualityInspection(Document):
|
||||
amended_from: DF.Link | None
|
||||
batch_no: DF.Link | None
|
||||
bom_no: DF.Link | None
|
||||
child_row_reference: DF.Data | None
|
||||
description: DF.SmallText | None
|
||||
inspected_by: DF.Link
|
||||
inspection_type: DF.Literal["", "Incoming", "Outgoing", "In Process"]
|
||||
@@ -74,6 +75,64 @@ class QualityInspection(Document):
|
||||
self.inspect_and_set_status()
|
||||
|
||||
self.validate_inspection_required()
|
||||
self.set_child_row_reference()
|
||||
|
||||
def set_child_row_reference(self):
|
||||
if self.child_row_reference:
|
||||
return
|
||||
|
||||
if not (self.reference_type and self.reference_name):
|
||||
return
|
||||
|
||||
doctype = self.reference_type + " Item"
|
||||
if self.reference_type == "Stock Entry":
|
||||
doctype = "Stock Entry Detail"
|
||||
|
||||
child_row_references = frappe.get_all(
|
||||
doctype,
|
||||
filters={"parent": self.reference_name, "item_code": self.item_code},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if not child_row_references:
|
||||
return
|
||||
|
||||
if len(child_row_references) == 1:
|
||||
self.child_row_reference = child_row_references[0]
|
||||
else:
|
||||
self.distribute_child_row_reference(child_row_references)
|
||||
|
||||
def distribute_child_row_reference(self, child_row_references):
|
||||
quality_inspections = frappe.get_all(
|
||||
"Quality Inspection",
|
||||
filters={
|
||||
"reference_name": self.reference_name,
|
||||
"item_code": self.item_code,
|
||||
"docstatus": ("<", 2),
|
||||
},
|
||||
fields=["name", "child_row_reference", "docstatus"],
|
||||
order_by="child_row_reference desc",
|
||||
)
|
||||
|
||||
for row in quality_inspections:
|
||||
if not child_row_references:
|
||||
break
|
||||
|
||||
if row.child_row_reference and row.child_row_reference in child_row_references:
|
||||
child_row_references.remove(row.child_row_reference)
|
||||
continue
|
||||
|
||||
if row.docstatus == 1:
|
||||
continue
|
||||
|
||||
if row.name == self.name:
|
||||
self.child_row_reference = child_row_references[0]
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
"Quality Inspection", row.name, "child_row_reference", child_row_references[0]
|
||||
)
|
||||
|
||||
child_row_references.remove(child_row_references[0])
|
||||
|
||||
def validate_inspection_required(self):
|
||||
if self.reference_type in ["Purchase Receipt", "Purchase Invoice"] and not frappe.get_cached_value(
|
||||
@@ -157,35 +216,38 @@ class QualityInspection(Document):
|
||||
)
|
||||
|
||||
else:
|
||||
args = [quality_inspection, self.modified, self.reference_name, self.item_code]
|
||||
doctype = self.reference_type + " Item"
|
||||
|
||||
if self.reference_type == "Stock Entry":
|
||||
doctype = "Stock Entry Detail"
|
||||
|
||||
if self.reference_type and self.reference_name:
|
||||
conditions = ""
|
||||
if doctype and self.reference_name:
|
||||
child_doc = frappe.qb.DocType(doctype)
|
||||
|
||||
query = (
|
||||
frappe.qb.update(child_doc)
|
||||
.set(child_doc.quality_inspection, quality_inspection)
|
||||
.where(
|
||||
(child_doc.parent == self.reference_name) & (child_doc.item_code == self.item_code)
|
||||
)
|
||||
)
|
||||
|
||||
if self.batch_no and self.docstatus == 1:
|
||||
conditions += " and t1.batch_no = %s"
|
||||
args.append(self.batch_no)
|
||||
query = query.where(child_doc.batch_no == self.batch_no)
|
||||
|
||||
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
|
||||
conditions += " and t1.quality_inspection = %s"
|
||||
args.append(self.name)
|
||||
query = query.where(child_doc.quality_inspection == self.name)
|
||||
|
||||
frappe.db.sql(
|
||||
f"""
|
||||
UPDATE
|
||||
`tab{doctype}` t1, `tab{self.reference_type}` t2
|
||||
SET
|
||||
t1.quality_inspection = %s, t2.modified = %s
|
||||
WHERE
|
||||
t1.parent = %s
|
||||
and t1.item_code = %s
|
||||
and t1.parent = t2.name
|
||||
{conditions}
|
||||
""",
|
||||
args,
|
||||
if self.child_row_reference:
|
||||
query = query.where(child_doc.name == self.child_row_reference)
|
||||
|
||||
query.run()
|
||||
|
||||
frappe.db.set_value(
|
||||
self.reference_type,
|
||||
self.reference_name,
|
||||
"modified",
|
||||
self.modified,
|
||||
)
|
||||
|
||||
def inspect_and_set_status(self):
|
||||
|
||||
@@ -166,7 +166,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
for qty, valuation in {10: 100, 20: 200}.items():
|
||||
stock_queue.append([qty, valuation])
|
||||
qty_after_transaction += qty
|
||||
balance_value += qty_after_transaction * valuation
|
||||
balance_value += qty * valuation
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
@@ -177,6 +177,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
"incoming_rate": valuation,
|
||||
"qty_after_transaction": qty_after_transaction,
|
||||
"stock_value_difference": valuation * qty,
|
||||
"stock_value": balance_value,
|
||||
"balance_value": balance_value,
|
||||
"valuation_rate": balance_value / qty_after_transaction,
|
||||
"actual_qty": qty,
|
||||
|
||||
@@ -3289,8 +3289,10 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N
|
||||
doc.append("entries", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1})
|
||||
|
||||
elif row.batches_to_be_consume:
|
||||
precision = frappe.get_precision("Serial and Batch Entry", "qty")
|
||||
doc.has_batch_no = 1
|
||||
for batch_no, qty in row.batches_to_be_consume.items():
|
||||
qty = flt(qty, precision)
|
||||
doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1})
|
||||
|
||||
if not doc.entries:
|
||||
|
||||
@@ -592,7 +592,10 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||
if tax.valid_from or tax.maximum_net_rate:
|
||||
# In purchase Invoice first preference will be given to supplier invoice date
|
||||
# if supplier date is not present then posting date
|
||||
validation_date = args.get("bill_date") or args.get("transaction_date")
|
||||
|
||||
validation_date = (
|
||||
args.get("bill_date") or args.get("posting_date") or args.get("transaction_date")
|
||||
)
|
||||
|
||||
if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax):
|
||||
taxes_with_validity.append(tax)
|
||||
|
||||
@@ -775,6 +775,9 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
|
||||
postprocess=post_process,
|
||||
)
|
||||
|
||||
if not target_doc.get("items"):
|
||||
add_po_items_to_pr(source_doc, target_doc)
|
||||
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
|
||||
@@ -794,3 +797,29 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
def add_po_items_to_pr(scr_doc, target_doc):
|
||||
fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items}
|
||||
|
||||
for (item_code, po_name), fg_qty in fg_items.items():
|
||||
po_doc = frappe.get_doc("Purchase Order", po_name)
|
||||
for item in po_doc.items:
|
||||
if item.fg_item != item_code:
|
||||
continue
|
||||
|
||||
qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty
|
||||
if qty:
|
||||
target_doc.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"qty": qty,
|
||||
"rate": item.rate,
|
||||
"warehouse": item.warehouse,
|
||||
"purchase_order": item.parent,
|
||||
"purchase_order_item": item.name,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1137,6 +1137,80 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
|
||||
self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
|
||||
|
||||
@change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
|
||||
def test_auto_create_purchase_receipt_with_no_reference_of_po_item(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
fg_item = "Subcontracted Item SA1"
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 5,
|
||||
},
|
||||
]
|
||||
|
||||
po = create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
do_not_submit=True,
|
||||
)
|
||||
po.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "_Test Account Excise Duty - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Excise Duty",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
sco = get_subcontracting_order(po_name=po.name)
|
||||
for row in sco.items:
|
||||
row.db_set("purchase_order_item", None)
|
||||
|
||||
sco.reload()
|
||||
|
||||
for row in sco.items:
|
||||
self.assertFalse(row.purchase_order_item)
|
||||
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
for row in scr.items:
|
||||
self.assertFalse(row.purchase_order_item)
|
||||
|
||||
scr.items[0].qty = 3
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
pr_details = frappe.get_all(
|
||||
"Purchase Receipt",
|
||||
filters={"subcontracting_receipt": scr.name},
|
||||
fields=["name", "total_taxes_and_charges"],
|
||||
)
|
||||
|
||||
self.assertTrue(pr_details)
|
||||
|
||||
pr_qty = frappe.db.get_value("Purchase Receipt Item", {"parent": pr_details[0]["name"]}, "qty")
|
||||
self.assertEqual(pr_qty, 6)
|
||||
|
||||
self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60)
|
||||
|
||||
def test_use_serial_batch_fields_for_subcontracting_receipt(self):
|
||||
fg_item = make_item(
|
||||
"Test Subcontracted Item With Batch No",
|
||||
|
||||
@@ -17,7 +17,7 @@ frappe.ready(function() {
|
||||
|
||||
if($("#footer-subscribe-email").val() && validate_email($("#footer-subscribe-email").val())) {
|
||||
$("#footer-subscribe-email").attr('disabled', true);
|
||||
$("#footer-subscribe-button").html("Sending...")
|
||||
$("#footer-subscribe-button").html(__("Sending..."))
|
||||
.attr("disabled", true);
|
||||
erpnext.subscribe_to_newsletter({
|
||||
email: $("#footer-subscribe-email").val(),
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
{% endif %}
|
||||
|
||||
{% for d in doc.taxes %}
|
||||
{% if d.base_tax_amount %}
|
||||
{% if d.tax_amount %}
|
||||
<div class="order-taxes w-100 mt-5">
|
||||
<div class="col-4 d-flex border-btm pb-5">
|
||||
<div class="item-grand-total col-8">
|
||||
{{ d.description }}
|
||||
</div>
|
||||
<div class="item-grand-total col-4 text-right pr-0">
|
||||
{{ d.get_formatted("base_tax_amount") }}
|
||||
{{ d.get_formatted("tax_amount") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<p>
|
||||
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
|
||||
class="btn btn-primary btn-sm" id="pay-for-order">
|
||||
{{ _("Pay") }} {{doc.get_formatted("grand_total") }}
|
||||
{{ _("Pay", null, "Amount") }} {{ pay_amount }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user