Merge pull request #32374 from frappe/version-13-hotfix
chore: release v13
This commit is contained in:
@@ -8,7 +8,11 @@ from frappe.model.document import Document
|
||||
from frappe.utils import flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
|
||||
from erpnext.accounts.utils import (
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
update_reference_in_payment_entry,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
|
||||
|
||||
@@ -190,6 +194,23 @@ class PaymentReconciliation(Document):
|
||||
inv.currency = entry.get("currency")
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, allocated_entry):
|
||||
if allocated_entry.get("reference_type") != "Payment Entry":
|
||||
return
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
row = self.get_payment_details(allocated_entry, dr_or_cr)
|
||||
|
||||
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
|
||||
update_reference_in_payment_entry(row, doc, do_not_save=True)
|
||||
|
||||
return doc.difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
def allocate_entries(self, args):
|
||||
self.validate_entries()
|
||||
@@ -205,12 +226,16 @@ class PaymentReconciliation(Document):
|
||||
res = self.get_allocated_entry(pay, inv, pay["amount"])
|
||||
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
|
||||
pay["amount"] = 0
|
||||
|
||||
res.difference_amount = self.get_difference_amount(res)
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
break
|
||||
elif inv.get("outstanding_amount") == 0:
|
||||
entries.append(res)
|
||||
continue
|
||||
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
@@ -1572,7 +1572,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-22 13:00:24.166684",
|
||||
"modified": "2022-09-27 13:00:24.166684",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.bold(d.warehouse),
|
||||
frappe.bold(d.qty),
|
||||
)
|
||||
if flt(available_stock) <= 0:
|
||||
if is_stock_item and flt(available_stock) <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif flt(available_stock) < flt(d.qty):
|
||||
elif is_stock_item and flt(available_stock) < flt(d.qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
|
||||
@@ -634,11 +634,12 @@ def get_stock_availability(item_code, warehouse):
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
return bin_qty - pos_sales_qty, is_stock_item
|
||||
else:
|
||||
is_stock_item = False
|
||||
is_stock_item = True
|
||||
if frappe.db.exists("Product Bundle", item_code):
|
||||
return get_bundle_availability(item_code, warehouse), is_stock_item
|
||||
else:
|
||||
# Is a service item
|
||||
is_stock_item = False
|
||||
# Is a service item or non_stock item
|
||||
return 0, is_stock_item
|
||||
|
||||
|
||||
@@ -652,7 +653,9 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles:
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
bundle_bin_qty = max_available_bundles
|
||||
|
||||
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
|
||||
@@ -744,3 +747,7 @@ def add_return_modes(doc, pos_profile):
|
||||
]:
|
||||
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
|
||||
append_payment(payment_mode[0])
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("POS Invoice", ["return_against"])
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-bordered" style="font-size: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
|
||||
@@ -513,7 +513,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
|
||||
@@ -1549,6 +1549,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].conversion_factor, 1000)
|
||||
|
||||
def test_batch_expiry_for_purchase_invoice(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
item = self.make_item(
|
||||
"_Test Batch Item For Return Check",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBIRC.#####",
|
||||
},
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
qty=1,
|
||||
item_code=item.name,
|
||||
update_stock=True,
|
||||
)
|
||||
|
||||
pi.load_from_db()
|
||||
batch_no = pi.items[0].batch_no
|
||||
self.assertTrue(batch_no)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1))
|
||||
|
||||
return_pi = make_return_doc(pi.doctype, pi.name)
|
||||
return_pi.save().submit()
|
||||
|
||||
self.assertTrue(return_pi.docstatus == 1)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -651,7 +651,6 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -2046,7 +2045,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-07-11 17:43:56.435382",
|
||||
"modified": "2022-09-16 17:44:22.227332",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -280,9 +280,9 @@ def get_conditions(filters):
|
||||
or filters.get("party")
|
||||
or filters.get("group_by") in ["Group by Account", "Group by Party"]
|
||||
):
|
||||
conditions.append("posting_date >=%(from_date)s")
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
|
||||
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
|
||||
conditions.append("(posting_date <=%(to_date)s)")
|
||||
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
@@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False):
|
||||
for d in data:
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
d[key] = totals[d["account"]]
|
||||
d["total"] = totals[d["account"]]
|
||||
return data
|
||||
|
||||
|
||||
@@ -19,14 +19,19 @@ def execute(filters=None):
|
||||
return _execute(filters)
|
||||
|
||||
|
||||
def _execute(filters=None, additional_table_columns=None, additional_query_columns=None):
|
||||
def _execute(
|
||||
filters=None,
|
||||
additional_table_columns=None,
|
||||
additional_query_columns=None,
|
||||
additional_conditions=None,
|
||||
):
|
||||
if not filters:
|
||||
filters = {}
|
||||
columns = get_columns(additional_table_columns, filters)
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
item_list = get_items(filters, additional_query_columns)
|
||||
item_list = get_items(filters, additional_query_columns, additional_conditions)
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
@@ -328,7 +333,7 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
def get_conditions(filters, additional_conditions=None):
|
||||
conditions = ""
|
||||
|
||||
for opts in (
|
||||
@@ -341,6 +346,9 @@ def get_conditions(filters):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
|
||||
if additional_conditions:
|
||||
conditions += additional_conditions
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
||||
where parent=`tabSales Invoice`.name
|
||||
@@ -376,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
|
||||
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns):
|
||||
conditions = get_conditions(filters)
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
conditions = get_conditions(filters, additional_conditions)
|
||||
|
||||
if additional_query_columns:
|
||||
additional_query_columns = ", " + ", ".join(additional_query_columns)
|
||||
|
||||
@@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
query_filters = {
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"report_type": report_type,
|
||||
"year_start_date": filters.year_start_date,
|
||||
"project": filters.project,
|
||||
@@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
where
|
||||
company=%(company)s
|
||||
{additional_conditions}
|
||||
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
|
||||
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
and account in (select name from `tabAccount` where report_type=%(report_type)s)
|
||||
and is_cancelled = 0
|
||||
group by account""".format(
|
||||
|
||||
@@ -106,12 +106,17 @@ def get_opening_balances(filters):
|
||||
where company=%(company)s
|
||||
and is_cancelled=0
|
||||
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
|
||||
and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes')
|
||||
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
|
||||
{account_filter}
|
||||
group by party""".format(
|
||||
account_filter=account_filter
|
||||
),
|
||||
{"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type},
|
||||
{
|
||||
"company": filters.company,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"party_type": filters.party_type,
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -439,7 +439,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1170,7 +1169,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-26 12:16:38.694276",
|
||||
"modified": "2022-09-16 17:45:04.954055",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -193,16 +193,16 @@ class BuyingController(StockController, Subcontracting):
|
||||
|
||||
if self.meta.get_field("base_in_words"):
|
||||
if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled():
|
||||
amount = self.base_rounded_total
|
||||
amount = abs(self.base_rounded_total)
|
||||
else:
|
||||
amount = self.base_grand_total
|
||||
amount = abs(self.base_grand_total)
|
||||
self.base_in_words = money_in_words(amount, self.company_currency)
|
||||
|
||||
if self.meta.get_field("in_words"):
|
||||
if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled():
|
||||
amount = self.rounded_total
|
||||
amount = abs(self.rounded_total)
|
||||
else:
|
||||
amount = self.grand_total
|
||||
amount = abs(self.grand_total)
|
||||
|
||||
self.in_words = money_in_words(amount, self.currency)
|
||||
|
||||
|
||||
@@ -478,7 +478,6 @@ scheduler_events = {
|
||||
],
|
||||
"hourly": [
|
||||
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails",
|
||||
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
@@ -487,6 +486,7 @@ scheduler_events = {
|
||||
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
|
||||
],
|
||||
"hourly_long": [
|
||||
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
||||
],
|
||||
|
||||
@@ -181,7 +181,6 @@ def add_data(
|
||||
total_l += 1
|
||||
elif status == "Half Day":
|
||||
total_p += 0.5
|
||||
total_a += 0.5
|
||||
total_l += 0.5
|
||||
elif not status:
|
||||
total_um += 1
|
||||
|
||||
@@ -829,6 +829,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
|
||||
.select(
|
||||
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
|
||||
item.item_name,
|
||||
item.name.as_("item_code"),
|
||||
bei.description,
|
||||
bei.stock_uom,
|
||||
item.min_order_qty,
|
||||
|
||||
@@ -557,37 +557,52 @@ erpnext.work_order = {
|
||||
|
||||
if(!frm.doc.skip_transfer){
|
||||
// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
|
||||
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
|
||||
&& frm.doc.status != 'Stopped') {
|
||||
frm.has_finish_btn = true;
|
||||
if (flt(doc.material_transferred_for_manufacturing) > 0 && frm.doc.status != 'Stopped') {
|
||||
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) {
|
||||
frm.has_finish_btn = true;
|
||||
|
||||
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
|
||||
// Only show "Material Consumption" when required_qty > consumed_qty
|
||||
var counter = 0;
|
||||
var tbl = frm.doc.required_items || [];
|
||||
var tbl_lenght = tbl.length;
|
||||
for (var i = 0, len = tbl_lenght; i < len; i++) {
|
||||
let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
|
||||
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
|
||||
counter += 1;
|
||||
if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) {
|
||||
// Only show "Material Consumption" when required_qty > consumed_qty
|
||||
var counter = 0;
|
||||
var tbl = frm.doc.required_items || [];
|
||||
var tbl_lenght = tbl.length;
|
||||
for (var i = 0, len = tbl_lenght; i < len; i++) {
|
||||
let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
|
||||
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
if (counter > 0) {
|
||||
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
|
||||
const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on;
|
||||
erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on);
|
||||
});
|
||||
consumption_btn.addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
if (counter > 0) {
|
||||
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
|
||||
const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on;
|
||||
erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on);
|
||||
});
|
||||
consumption_btn.addClass('btn-primary');
|
||||
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
|
||||
if(doc.material_transferred_for_manufacturing>=doc.qty) {
|
||||
// all materials transferred for manufacturing, make this primary
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
|
||||
let allowance_percentage = doc.overproduction_percentage_for_work_order;
|
||||
|
||||
var finish_btn = frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
if (allowance_percentage > 0) {
|
||||
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
|
||||
|
||||
if(doc.material_transferred_for_manufacturing>=doc.qty) {
|
||||
// all materials transferred for manufacturing, make this primary
|
||||
finish_btn.addClass('btn-primary');
|
||||
if ((flt(doc.produced_qty) < allowed_qty)) {
|
||||
frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -64,22 +64,21 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
cond = "1=1"
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
query = (
|
||||
frappe.qb.from_(wo)
|
||||
.select(wo.name.as_("work_order"), wo.qty, wo.produced_qty, wo.production_item, wo.bom_no)
|
||||
.where((wo.produced_qty > wo.qty) & (wo.docstatus == 1))
|
||||
)
|
||||
|
||||
if filters.get("bom_no") and not filters.get("work_order"):
|
||||
cond += " and bom_no = '%s'" % filters.get("bom_no")
|
||||
query = query.where(wo.bom_no == filters.get("bom_no"))
|
||||
|
||||
if filters.get("work_order"):
|
||||
cond += " and name = '%s'" % filters.get("work_order")
|
||||
query = query.where(wo.name == filters.get("work_order"))
|
||||
|
||||
results = []
|
||||
for d in frappe.db.sql(
|
||||
""" select name as work_order, qty, produced_qty, production_item, bom_no
|
||||
from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(
|
||||
cond
|
||||
),
|
||||
as_dict=1,
|
||||
):
|
||||
for d in query.run(as_dict=True):
|
||||
results.append(d)
|
||||
|
||||
for data in frappe.get_all(
|
||||
@@ -95,16 +94,17 @@ def get_data(filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_work_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
cond = "1=1"
|
||||
if filters.get("bom_no"):
|
||||
cond += " and bom_no = '%s'" % filters.get("bom_no")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select name from `tabWork Order`
|
||||
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1
|
||||
order by name limit {1}, {2}""".format(
|
||||
cond, start, page_len
|
||||
),
|
||||
{"name": "%%%s%%" % txt},
|
||||
as_list=1,
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
query = (
|
||||
frappe.qb.from_(wo)
|
||||
.select(wo.name)
|
||||
.where((wo.name.like(f"{txt}%")) & (wo.produced_qty > wo.qty) & (wo.docstatus == 1))
|
||||
.orderby(wo.name)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
|
||||
if filters.get("bom_no"):
|
||||
query = query.where(wo.bom_no == filters.get("bom_no"))
|
||||
|
||||
return query.run(as_list=True)
|
||||
|
||||
@@ -96,38 +96,39 @@ class ForecastingReport(ExponentialSmoothingForecast):
|
||||
value["avg"] = flt(sum(list_of_period_value)) / flt(sum(total_qty))
|
||||
|
||||
def get_data_for_forecast(self):
|
||||
cond = ""
|
||||
if self.filters.item_code:
|
||||
cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code))
|
||||
|
||||
warehouses = []
|
||||
if self.filters.warehouse:
|
||||
warehouses = get_child_warehouses(self.filters.warehouse)
|
||||
cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses)))
|
||||
|
||||
input_data = [self.filters.from_date, self.filters.company]
|
||||
if warehouses:
|
||||
input_data.extend(warehouses)
|
||||
parent = frappe.qb.DocType(self.doctype)
|
||||
child = frappe.qb.DocType(self.child_doctype)
|
||||
|
||||
date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
so.{date_field} as posting_date, soi.item_code, soi.warehouse,
|
||||
soi.item_name, soi.stock_qty as qty, soi.base_amount as amount
|
||||
FROM
|
||||
`tab{doc}` so, `tab{child_doc}` soi
|
||||
WHERE
|
||||
so.docstatus = 1 AND so.name = soi.parent AND
|
||||
so.{date_field} < %s AND so.company = %s {cond}
|
||||
""".format(
|
||||
doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond
|
||||
),
|
||||
tuple(input_data),
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
.from_(child)
|
||||
.select(
|
||||
parent[date_field].as_("posting_date"),
|
||||
child.item_code,
|
||||
child.warehouse,
|
||||
child.item_name,
|
||||
child.stock_qty.as_("qty"),
|
||||
child.base_amount.as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(parent.docstatus == 1)
|
||||
& (parent.name == child.parent)
|
||||
& (parent[date_field] < self.filters.from_date)
|
||||
& (parent.company == self.filters.company)
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.item_code:
|
||||
query = query.where(child.item_code == self.filters.item_code)
|
||||
|
||||
if self.filters.warehouse:
|
||||
warehouses = get_child_warehouses(self.filters.warehouse) or []
|
||||
query = query.where(child.warehouse.isin(warehouses))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def prepare_final_data(self):
|
||||
self.data = []
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Dict, List, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
Filters = frappe._dict
|
||||
Row = frappe._dict
|
||||
@@ -14,15 +15,50 @@ QueryArgs = Dict[str, str]
|
||||
|
||||
|
||||
def execute(filters: Filters) -> Tuple[Columns, Data]:
|
||||
filters = frappe._dict(filters or {})
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_data(filters: Filters) -> Data:
|
||||
query_args = get_query_args(filters)
|
||||
data = run_query(query_args)
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(wo)
|
||||
.inner_join(se)
|
||||
.on(wo.name == se.work_order)
|
||||
.select(
|
||||
wo.name,
|
||||
wo.status,
|
||||
wo.production_item,
|
||||
wo.qty,
|
||||
wo.produced_qty,
|
||||
wo.process_loss_qty,
|
||||
(wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"),
|
||||
Sum(se.total_incoming_value).as_("total_fg_value"),
|
||||
Sum(se.total_outgoing_value).as_("total_rm_value"),
|
||||
)
|
||||
.where(
|
||||
(wo.process_loss_qty > 0)
|
||||
& (wo.company == filters.company)
|
||||
& (se.docstatus == 1)
|
||||
& (se.posting_date.between(filters.from_date, filters.to_date))
|
||||
)
|
||||
.groupby(se.work_order)
|
||||
)
|
||||
|
||||
if "item" in filters:
|
||||
query.where(wo.production_item == filters.item)
|
||||
|
||||
if "work_order" in filters:
|
||||
query.where(wo.name == filters.work_order)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
update_data_with_total_pl_value(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -67,54 +103,7 @@ def get_columns() -> Columns:
|
||||
]
|
||||
|
||||
|
||||
def get_query_args(filters: Filters) -> QueryArgs:
|
||||
query_args = {}
|
||||
query_args.update(filters)
|
||||
query_args.update(get_filter_conditions(filters))
|
||||
return query_args
|
||||
|
||||
|
||||
def run_query(query_args: QueryArgs) -> Data:
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
wo.name, wo.status, wo.production_item, wo.qty,
|
||||
wo.produced_qty, wo.process_loss_qty,
|
||||
(wo.produced_qty - wo.process_loss_qty) as actual_produced_qty,
|
||||
sum(se.total_incoming_value) as total_fg_value,
|
||||
sum(se.total_outgoing_value) as total_rm_value
|
||||
FROM
|
||||
`tabWork Order` wo INNER JOIN `tabStock Entry` se
|
||||
ON wo.name=se.work_order
|
||||
WHERE
|
||||
process_loss_qty > 0
|
||||
AND wo.company = %(company)s
|
||||
AND se.docstatus = 1
|
||||
AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s
|
||||
{item_filter}
|
||||
{work_order_filter}
|
||||
GROUP BY
|
||||
se.work_order
|
||||
""".format(
|
||||
**query_args
|
||||
),
|
||||
query_args,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def update_data_with_total_pl_value(data: Data) -> None:
|
||||
for row in data:
|
||||
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
|
||||
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
|
||||
|
||||
|
||||
def get_filter_conditions(filters: Filters) -> QueryArgs:
|
||||
filter_conditions = dict(item_filter="", work_order_filter="")
|
||||
if "item" in filters:
|
||||
production_item = filters.get("item")
|
||||
filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"})
|
||||
if "work_order" in filters:
|
||||
work_order_name = filters.get("work_order")
|
||||
filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"})
|
||||
return filter_conditions
|
||||
|
||||
@@ -4,42 +4,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||
|
||||
# and bom_no is not null and bom_no !=''
|
||||
|
||||
mapper = {
|
||||
"Sales Order": {
|
||||
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
|
||||
stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse,
|
||||
`tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """,
|
||||
"filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty
|
||||
and `tabSales Order`.per_delivered < 100.0""",
|
||||
},
|
||||
"Material Request": {
|
||||
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
|
||||
stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse,
|
||||
`tabMaterial Request Item`.schedule_date """,
|
||||
"filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100
|
||||
and `tabMaterial Request`.material_request_type = 'Manufacture' """,
|
||||
},
|
||||
"Work Order": {
|
||||
"fields": """ production_item, item_name as production_item_name, planned_start_date,
|
||||
stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """,
|
||||
"filters": "docstatus = 1 and status not in ('Completed', 'Stopped')",
|
||||
},
|
||||
}
|
||||
|
||||
order_mapper = {
|
||||
"Sales Order": {
|
||||
"Delivery Date": "`tabSales Order Item`.delivery_date asc",
|
||||
"Total Amount": "`tabSales Order`.base_grand_total desc",
|
||||
},
|
||||
"Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"},
|
||||
"Work Order": {"Planned Start Date": "planned_start_date asc"},
|
||||
}
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
return ProductionPlanReport(filters).execute_report()
|
||||
@@ -63,40 +31,78 @@ class ProductionPlanReport(object):
|
||||
return self.columns, self.data
|
||||
|
||||
def get_open_orders(self):
|
||||
doctype = (
|
||||
"`tabWork Order`"
|
||||
if self.filters.based_on == "Work Order"
|
||||
else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on)
|
||||
)
|
||||
doctype, order_by = self.filters.based_on, self.filters.order_by
|
||||
|
||||
filters = mapper.get(self.filters.based_on)["filters"]
|
||||
filters = self.prepare_other_conditions(filters, self.filters.based_on)
|
||||
order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by])
|
||||
parent = frappe.qb.DocType(doctype)
|
||||
query = None
|
||||
|
||||
self.orders = frappe.db.sql(
|
||||
""" SELECT {fields} from {doctype}
|
||||
WHERE {filters} {order_by}""".format(
|
||||
doctype=doctype,
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
fields=mapper.get(self.filters.based_on)["fields"],
|
||||
),
|
||||
tuple(self.filters.docnames),
|
||||
as_dict=1,
|
||||
)
|
||||
if doctype == "Work Order":
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
.select(
|
||||
parent.production_item,
|
||||
parent.item_name.as_("production_item_name"),
|
||||
parent.planned_start_date,
|
||||
parent.stock_uom,
|
||||
parent.qty.as_("qty_to_manufacture"),
|
||||
parent.name,
|
||||
parent.bom_no,
|
||||
parent.fg_warehouse.as_("warehouse"),
|
||||
)
|
||||
.where(parent.status.notin(["Completed", "Stopped"]))
|
||||
)
|
||||
|
||||
def prepare_other_conditions(self, filters, doctype):
|
||||
if self.filters.docnames:
|
||||
field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype)
|
||||
filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames)))
|
||||
if order_by == "Planned Start Date":
|
||||
query = query.orderby(parent.planned_start_date, order=Order.asc)
|
||||
|
||||
if doctype != "Work Order":
|
||||
filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype)
|
||||
if self.filters.docnames:
|
||||
query = query.where(parent.name.isin(self.filters.docnames))
|
||||
|
||||
else:
|
||||
child = frappe.qb.DocType(f"{doctype} Item")
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
.from_(child)
|
||||
.select(
|
||||
child.bom_no,
|
||||
child.stock_uom,
|
||||
child.warehouse,
|
||||
child.parent.as_("name"),
|
||||
child.item_code.as_("production_item"),
|
||||
child.stock_qty.as_("qty_to_manufacture"),
|
||||
child.item_name.as_("production_item_name"),
|
||||
)
|
||||
.where(parent.name == child.parent)
|
||||
)
|
||||
|
||||
if self.filters.docnames:
|
||||
query = query.where(child.parent.isin(self.filters.docnames))
|
||||
|
||||
if doctype == "Sales Order":
|
||||
query = query.select(
|
||||
child.delivery_date,
|
||||
parent.base_grand_total,
|
||||
).where((child.stock_qty > child.produced_qty) & (parent.per_delivered < 100.0))
|
||||
|
||||
if order_by == "Delivery Date":
|
||||
query = query.orderby(child.delivery_date, order=Order.asc)
|
||||
elif order_by == "Total Amount":
|
||||
query = query.orderby(parent.base_grand_total, order=Order.desc)
|
||||
|
||||
elif doctype == "Material Request":
|
||||
query = query.select(child.schedule_date,).where(
|
||||
(parent.per_ordered < 100) & (parent.material_request_type == "Manufacture")
|
||||
)
|
||||
|
||||
if order_by == "Required Date":
|
||||
query = query.orderby(child.schedule_date, order=Order.asc)
|
||||
|
||||
query = query.where(parent.docstatus == 1)
|
||||
|
||||
if self.filters.company:
|
||||
filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company))
|
||||
query = query.where(parent.company == self.filters.company)
|
||||
|
||||
return filters
|
||||
self.orders = query.run(as_dict=True)
|
||||
|
||||
def get_raw_materials(self):
|
||||
if not self.orders:
|
||||
@@ -134,29 +140,29 @@ class ProductionPlanReport(object):
|
||||
|
||||
bom_nos.append(bom_no)
|
||||
|
||||
bom_doctype = (
|
||||
bom_item_doctype = (
|
||||
"BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item"
|
||||
)
|
||||
|
||||
qty_field = (
|
||||
"qty_consumed_per_unit"
|
||||
if self.filters.include_subassembly_raw_materials
|
||||
else "(bom_item.qty / bom.quantity)"
|
||||
)
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType(bom_item_doctype)
|
||||
|
||||
raw_materials = frappe.db.sql(
|
||||
""" SELECT bom_item.parent, bom_item.item_code,
|
||||
bom_item.item_name as raw_material_name, {0} as required_qty_per_unit
|
||||
FROM
|
||||
`tabBOM` as bom, `tab{1}` as bom_item
|
||||
WHERE
|
||||
bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1
|
||||
""".format(
|
||||
qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos))
|
||||
),
|
||||
tuple(bom_nos),
|
||||
as_dict=1,
|
||||
)
|
||||
if self.filters.include_subassembly_raw_materials:
|
||||
qty_field = bom_item.qty_consumed_per_unit
|
||||
else:
|
||||
qty_field = bom_item.qty / bom.quantity
|
||||
|
||||
raw_materials = (
|
||||
frappe.qb.from_(bom)
|
||||
.from_(bom_item)
|
||||
.select(
|
||||
bom_item.parent,
|
||||
bom_item.item_code,
|
||||
bom_item.item_name.as_("raw_material_name"),
|
||||
qty_field.as_("required_qty_per_unit"),
|
||||
)
|
||||
.where((bom_item.parent.isin(bom_nos)) & (bom_item.parent == bom.name) & (bom.docstatus == 1))
|
||||
).run(as_dict=True)
|
||||
|
||||
if not raw_materials:
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@@ -16,70 +17,70 @@ def execute(filters=None):
|
||||
def get_item_list(wo_list, filters):
|
||||
out = []
|
||||
|
||||
# Add a row for each item/qty
|
||||
for wo_details in wo_list:
|
||||
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
|
||||
if wo_list:
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType("BOM Item")
|
||||
|
||||
for wo_item_details in frappe.db.get_values(
|
||||
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
|
||||
):
|
||||
# Add a row for each item/qty
|
||||
for wo_details in wo_list:
|
||||
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
|
||||
|
||||
item_list = frappe.db.sql(
|
||||
"""SELECT
|
||||
bom_item.item_code as item_code,
|
||||
ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty
|
||||
FROM
|
||||
`tabBOM` as bom, `tabBOM Item` AS bom_item
|
||||
LEFT JOIN `tabBin` AS ledger
|
||||
ON bom_item.item_code = ledger.item_code
|
||||
AND ledger.warehouse = ifnull(%(warehouse)s,%(filterhouse)s)
|
||||
WHERE
|
||||
bom.name = bom_item.parent
|
||||
and bom_item.item_code = %(item_code)s
|
||||
and bom.name = %(bom)s
|
||||
GROUP BY
|
||||
bom_item.item_code""",
|
||||
{
|
||||
"bom": wo_details.bom_no,
|
||||
"warehouse": wo_item_details.source_warehouse,
|
||||
"filterhouse": filters.warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
for wo_item_details in frappe.db.get_values(
|
||||
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
|
||||
):
|
||||
item_list = (
|
||||
frappe.qb.from_(bom)
|
||||
.from_(bom_item)
|
||||
.left_join(bin)
|
||||
.on(
|
||||
(bom_item.item_code == bin.item_code)
|
||||
& (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse))
|
||||
)
|
||||
.select(
|
||||
bom_item.item_code.as_("item_code"),
|
||||
IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"),
|
||||
)
|
||||
.where(
|
||||
(bom.name == bom_item.parent)
|
||||
& (bom_item.item_code == wo_item_details.item_code)
|
||||
& (bom.name == wo_details.bom_no)
|
||||
)
|
||||
.groupby(bom_item.item_code)
|
||||
).run(as_dict=1)
|
||||
|
||||
stock_qty = 0
|
||||
count = 0
|
||||
buildable_qty = wo_details.qty
|
||||
for item in item_list:
|
||||
count = count + 1
|
||||
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
|
||||
stock_qty = stock_qty + 1
|
||||
elif buildable_qty >= item.build_qty:
|
||||
buildable_qty = item.build_qty
|
||||
stock_qty = 0
|
||||
count = 0
|
||||
buildable_qty = wo_details.qty
|
||||
for item in item_list:
|
||||
count = count + 1
|
||||
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
|
||||
stock_qty = stock_qty + 1
|
||||
elif buildable_qty >= item.build_qty:
|
||||
buildable_qty = item.build_qty
|
||||
|
||||
if count == stock_qty:
|
||||
build = "Y"
|
||||
else:
|
||||
build = "N"
|
||||
if count == stock_qty:
|
||||
build = "Y"
|
||||
else:
|
||||
build = "N"
|
||||
|
||||
row = frappe._dict(
|
||||
{
|
||||
"work_order": wo_details.name,
|
||||
"status": wo_details.status,
|
||||
"req_items": cint(count),
|
||||
"instock": stock_qty,
|
||||
"description": desc,
|
||||
"source_warehouse": wo_item_details.source_warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
"bom_no": wo_details.bom_no,
|
||||
"qty": wo_details.qty,
|
||||
"buildable_qty": buildable_qty,
|
||||
"ready_to_build": build,
|
||||
}
|
||||
)
|
||||
row = frappe._dict(
|
||||
{
|
||||
"work_order": wo_details.name,
|
||||
"status": wo_details.status,
|
||||
"req_items": cint(count),
|
||||
"instock": stock_qty,
|
||||
"description": desc,
|
||||
"source_warehouse": wo_item_details.source_warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
"bom_no": wo_details.bom_no,
|
||||
"qty": wo_details.qty,
|
||||
"buildable_qty": buildable_qty,
|
||||
"ready_to_build": build,
|
||||
}
|
||||
)
|
||||
|
||||
out.append(row)
|
||||
out.append(row)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -894,6 +894,7 @@ class GSPConnector:
|
||||
return self.e_invoice_settings.auth_token
|
||||
|
||||
def make_request(self, request_type, url, headers=None, data=None):
|
||||
res = None
|
||||
try:
|
||||
if request_type == "post":
|
||||
res = make_post_request(url, headers=headers, data=data)
|
||||
|
||||
@@ -15,12 +15,6 @@ filters = filters.concat({
|
||||
"placeholder":"Company GSTIN",
|
||||
"options": [""],
|
||||
"width": "80"
|
||||
}, {
|
||||
"fieldname":"invoice_type",
|
||||
"label": __("Invoice Type"),
|
||||
"fieldtype": "Select",
|
||||
"placeholder":"Invoice Type",
|
||||
"options": ["", "Regular", "SEZ", "Export", "Deemed Export"]
|
||||
});
|
||||
|
||||
// Handle company on change
|
||||
|
||||
@@ -5,31 +5,48 @@
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import _execute
|
||||
|
||||
|
||||
def get_conditions(filters, additional_query_columns):
|
||||
conditions = ""
|
||||
|
||||
for opts in additional_query_columns:
|
||||
if filters.get(opts):
|
||||
conditions += f" and {opts}=%({opts})s"
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
additional_table_columns = [
|
||||
dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120),
|
||||
dict(
|
||||
fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140
|
||||
),
|
||||
dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120),
|
||||
dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120),
|
||||
dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120),
|
||||
dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120),
|
||||
dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120),
|
||||
dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130),
|
||||
dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120),
|
||||
]
|
||||
|
||||
additional_query_columns = [
|
||||
"customer_gstin",
|
||||
"billing_address_gstin",
|
||||
"company_gstin",
|
||||
"place_of_supply",
|
||||
"reverse_charge",
|
||||
"gst_category",
|
||||
"export_type",
|
||||
"ecommerce_gstin",
|
||||
"gst_hsn_code",
|
||||
]
|
||||
|
||||
additional_conditions = get_conditions(filters, additional_query_columns)
|
||||
|
||||
return _execute(
|
||||
filters,
|
||||
additional_table_columns=[
|
||||
dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120),
|
||||
dict(
|
||||
fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140
|
||||
),
|
||||
dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120),
|
||||
dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120),
|
||||
dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120),
|
||||
dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120),
|
||||
dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120),
|
||||
dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130),
|
||||
dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120),
|
||||
],
|
||||
additional_query_columns=[
|
||||
"customer_gstin",
|
||||
"billing_address_gstin",
|
||||
"company_gstin",
|
||||
"place_of_supply",
|
||||
"reverse_charge",
|
||||
"gst_category",
|
||||
"export_type",
|
||||
"ecommerce_gstin",
|
||||
"gst_hsn_code",
|
||||
],
|
||||
additional_table_columns=additional_table_columns,
|
||||
additional_query_columns=additional_query_columns,
|
||||
additional_conditions=additional_conditions,
|
||||
)
|
||||
|
||||
@@ -461,7 +461,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1,
|
||||
"show_days": 1,
|
||||
@@ -1174,7 +1173,7 @@
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-15 20:35:32.635804",
|
||||
"modified": "2022-09-16 17:44:43.221804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
||||
@@ -544,7 +544,6 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1549,7 +1548,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-10 03:52:22.212953",
|
||||
"modified": "2022-09-16 17:43:57.007441",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
||||
@@ -660,7 +660,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (available_qty < qty_needed) {
|
||||
} else if (is_stock_item && available_qty < qty_needed) {
|
||||
frappe.throw({
|
||||
message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
|
||||
indicator: 'orange'
|
||||
@@ -694,7 +694,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
callback(res) {
|
||||
if (!me.item_stock_map[item_code])
|
||||
me.item_stock_map[item_code] = {};
|
||||
me.item_stock_map[item_code][warehouse] = res.message[0];
|
||||
me.item_stock_map[item_code][warehouse] = res.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,13 +242,14 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
if (this.value) {
|
||||
me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
|
||||
me.item_stock_map = me.events.get_item_stock_map();
|
||||
const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
|
||||
const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0];
|
||||
const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]);
|
||||
if (available_qty === undefined) {
|
||||
me.events.get_available_stock(me.item_row.item_code, this.value).then(() => {
|
||||
// item stock map is updated now reset warehouse
|
||||
me.warehouse_control.set_value(this.value);
|
||||
})
|
||||
} else if (available_qty === 0) {
|
||||
} else if (available_qty === 0 && is_stock_item) {
|
||||
me.warehouse_control.set_value('');
|
||||
const bold_item_code = me.item_row.item_code.bold();
|
||||
const bold_warehouse = this.value.bold();
|
||||
|
||||
@@ -490,7 +490,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1336,7 +1335,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-10 03:52:04.197415",
|
||||
"modified": "2022-09-16 17:46:17.701904",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -404,7 +404,6 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1149,7 +1148,7 @@
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-27 15:59:18.550583",
|
||||
"modified": "2022-09-16 17:45:58.430132",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
||||
@@ -1446,6 +1446,37 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
self.assertEqual(query[0].value, 0)
|
||||
|
||||
def test_batch_expiry_for_purchase_receipt(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
item = make_item(
|
||||
"_Test Batch Item For Return Check",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBIRC.#####",
|
||||
},
|
||||
)
|
||||
|
||||
pi = make_purchase_receipt(
|
||||
qty=1,
|
||||
item_code=item.name,
|
||||
update_stock=True,
|
||||
)
|
||||
|
||||
pi.load_from_db()
|
||||
batch_no = pi.items[0].batch_no
|
||||
self.assertTrue(batch_no)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1))
|
||||
|
||||
return_pi = make_return_doc(pi.doctype, pi.name)
|
||||
return_pi.save().submit()
|
||||
|
||||
self.assertTrue(return_pi.docstatus == 1)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -1015,8 +1015,8 @@ class StockEntry(StockController):
|
||||
# No work order could mean independent Manufacture entry, if so skip validation
|
||||
if self.work_order and self.fg_completed_qty > allowed_qty:
|
||||
frappe.throw(
|
||||
_("For quantity {0} should not be greater than work order quantity {1}").format(
|
||||
flt(self.fg_completed_qty), wo_qty
|
||||
_("For quantity {0} should not be greater than allowed quantity {1}").format(
|
||||
flt(self.fg_completed_qty), allowed_qty
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -153,6 +153,9 @@ class StockLedgerEntry(Document):
|
||||
|
||||
def validate_batch(self):
|
||||
if self.batch_no and self.voucher_type != "Stock Entry":
|
||||
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0:
|
||||
return
|
||||
|
||||
expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date")
|
||||
if expiry_date:
|
||||
if getdate(self.posting_date) > getdate(expiry_date):
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Field
|
||||
from frappe.query_builder.functions import Min, Timestamp
|
||||
from frappe.utils import add_days, getdate, today
|
||||
from six import iteritems
|
||||
|
||||
@@ -29,7 +31,7 @@ def execute(filters=None):
|
||||
def get_unsync_date(filters):
|
||||
date = filters.from_date
|
||||
if not date:
|
||||
date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""")
|
||||
date = (frappe.qb.from_("Stock Ledger Entry").select(Min(Field("posting_date")))).run()
|
||||
date = date[0][0]
|
||||
|
||||
if not date:
|
||||
@@ -55,22 +57,27 @@ def get_data(report_filters):
|
||||
result = []
|
||||
|
||||
voucher_wise_dict = {}
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
name, posting_date, posting_time, voucher_type, voucher_no,
|
||||
stock_value_difference, stock_value, warehouse, item_code
|
||||
FROM
|
||||
`tabStock Ledger Entry`
|
||||
WHERE
|
||||
posting_date
|
||||
= %s and company = %s
|
||||
and is_cancelled = 0
|
||||
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
|
||||
""",
|
||||
(from_date, report_filters.company),
|
||||
as_dict=1,
|
||||
)
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
data = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(
|
||||
sle.name,
|
||||
sle.posting_date,
|
||||
sle.posting_time,
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
sle.stock_value_difference,
|
||||
sle.stock_value,
|
||||
sle.warehouse,
|
||||
sle.item_code,
|
||||
)
|
||||
.where(
|
||||
(sle.posting_date == from_date)
|
||||
& (sle.company == report_filters.company)
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation)
|
||||
).run(as_dict=True)
|
||||
|
||||
for d in data:
|
||||
voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)
|
||||
|
||||
@@ -62,22 +62,28 @@ def get_data(filters, columns):
|
||||
|
||||
|
||||
def get_item_price_qty_data(filters):
|
||||
conditions = ""
|
||||
if filters.get("item_code"):
|
||||
conditions += "where a.item_code=%(item_code)s"
|
||||
item_price = frappe.qb.DocType("Item Price")
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
|
||||
item_results = frappe.db.sql(
|
||||
"""select a.item_code, a.item_name, a.name as price_list_name,
|
||||
a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty
|
||||
from `tabItem Price` a left join `tabBin` b
|
||||
ON a.item_code = b.item_code
|
||||
{conditions}""".format(
|
||||
conditions=conditions
|
||||
),
|
||||
filters,
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(item_price)
|
||||
.left_join(bin)
|
||||
.on(item_price.item_code == bin.item_code)
|
||||
.select(
|
||||
item_price.item_code,
|
||||
item_price.item_name,
|
||||
item_price.name.as_("price_list_name"),
|
||||
item_price.brand.as_("brand"),
|
||||
bin.warehouse.as_("warehouse"),
|
||||
bin.actual_qty.as_("actual_qty"),
|
||||
)
|
||||
)
|
||||
|
||||
if filters.get("item_code"):
|
||||
query = query.where(item_price.item_code == filters.get("item_code"))
|
||||
|
||||
item_results = query.run(as_dict=True)
|
||||
|
||||
price_list_names = list(set(item.price_list_name for item in item_results))
|
||||
|
||||
buying_price_map = get_price_map(price_list_names, buying=1)
|
||||
|
||||
Reference in New Issue
Block a user